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>
12 KiB
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-readywith 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)jp2aascii-image-converterimg2txt
- 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-configwith{ 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
dataUrlback to Blob viafetch(), sends asmultipart/form-data - POSTs to
/api/convert(proxied by Vite dev server tohttp://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
// 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
- Parse multipart body — extract image file + JSON fields (
tool,flags) - Write image to temp file (
/tmp/asciinator-<uuid>.<ext>) - Pass through ImageMagick preprocessing (see §4.1)
- Invoke selected tool via
converters.js - Stream stdout back as
text/plain - Clean up temp files in
finally - 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>) jp2areceives 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
execafor all invocations - Map the
flagsobject 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.cssfor 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.vuetemplates emmet-lsexpansion in<template>blocks- ESLint auto-fix on save (
BufWritePreautocmd) - Prettier format on save — confirm it doesn't fight ESLint
gd(go-to-definition) across.vueSFC 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:3001
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
localStoragefor anything - Always use
execafor shell invocations — neverexec()or rawspawn() - 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) andsrc/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)