Files
markdown-converter/docs/superpowers/specs/2026-09-14-ascii-art-upgrade-design.md
T
amitwh 43c26c6521 docs(specs): add theme-registry + ascii-art-upgrade + flowchart-editor designs
Three new specs for the v4.8.0+ feature wave:

* Theme registry: replace hardcoded 25-theme menu in main.js with a pure
  ThemeRegistry module + per-theme CSS file convention. Add 12 new themes
  (Catppuccin x4, One Light, Tokyo Night Storm, Synthwave '84, Outrun,
  Winter is Coming Light+Dark, Solarized Dark HC, Spring Light).
* ASCII art upgrade: consolidate dual implementations (standalone window
  vs dead in-app modal) into a single path; add 12 hand-coded fonts +
  figlet npm library for 400+ fonts; add copy/save/insert output
  destinations; comprehensive tests for the previously-zero-coverage
  textToASCII/createASCIIBox/getASCIITemplate machinery.
* Flow chart editor: sidebar panel with hand-rolled SVG canvas, node-graph
  data model, drag/drop editing, live Mermaid source preview, undo/redo,
  session persistence. Emits Mermaid which the existing preview pane
  already renders natively.

Amit Haridas
2026-09-14 19:01:09 +05:30

16 KiB
Raw Blame History

MarkdownConverter — ASCII Art Generator Upgrade Design

Date: 2026-09-14 Status: Draft — pending review Author: Amit Haridas

Overview

Promote the ASCII Art Generator from "works but untested, dual-implemented, 5 fonts" to a full-fledged feature with 17 hand-coded fonts + 400+ FIGlet fonts, single source of truth, comprehensive test coverage, and three output paths (insert at cursor, copy to clipboard, save to file). Consolidate the dead-code in-app modal in favor of the existing standalone BrowserWindow.

Goals

  1. Single implementation path. Delete the dead #ascii-art-dialog modal markup, its ModalManager instance, the showASCIIGenerator* dead channels in preload.js, and the corresponding renderer controller at renderer.js:5942-6727. The standalone ascii-generator.html window remains the only path.
  2. Many more fonts. Add 12 hand-coded fonts (Big, Small, Lean, Slant, Isometric1-4, 3-D, 3x5, ANSI Shadow, Calvin S) on top of the 5 existing (standard, banner, block, bubble, digital). Plus all figlet npm fonts (400+) accessible through a searchable picker.
  3. Three output destinations. Insert at cursor (existing — wraps result in a fenced code block), Copy to clipboard (new), Save to .txt file (new).
  4. Comprehensive test coverage. The current textToASCII, createASCIIBox, getASCIITemplate, and the new figlet adapter all have ZERO tests today. Add full coverage — snapshot tests for known outputs across all fonts, integration tests for copy/save/insert.
  5. Pure-module architecture. Move all algorithm code from renderer.js into src/main/AsciiArt.js (pure module, IPC-coupled via main.js, mirroring the DocQA/DailyNotes pattern).

Non-Goals (v1)

  • No animated ASCII / motion ASCII.
  • No color / ANSI escape codes (would break the markdown fenced code block contract).
  • No font upload — only shipped fonts (hand-coded + figlet standard library).
  • No image export (PNG/SVG of ASCII art) — text only.
  • No undo history inside the modal — single-shot generation.
  • No in-modal text editing of the generated ASCII (the user can paste into the editor instead).

Decisions Locked

Decision Choice Reason
Library figlet npm package (latest, ~400 KB unpacked) Industry-standard JS port of the original C library; 400+ fonts; small footprint
Hand-coded fonts Add 12 more, keep existing 5 as legacy/ set Hand-coded fonts have a distinct aesthetic (figlet doesn't 1:1 replicate all of them) and load with zero I/O
Font loading Hand-coded: synchronous. figlet: lazy-loaded on font-picker open (async) Avoids 400 KB of synchronous font loading at app startup
Module split src/main/AsciiArt.js (pure), src/renderer/ascii-controller.js (UI), src/ascii-generator.html (already exists) Matches established renderer-controller + main-module split
Single source of truth Standalone window only; in-app modal deleted Dead code is technical debt; consolidation is required for testability
Output destinations Insert at cursor (existing) + Copy clipboard + Save to file Covers all "I want this ASCII art in my markdown" use cases
Insert wrapper Result wrapped in \n\``\n…\n```\n` (existing behaviour) Preserves current markdown-fence contract
Persistence None — the modal is stateless. Last-used font remembered via electron-store ascii:lastFont key (new). Small UX win without adding persistence layer
Tests jsdom + Jest, snapshot tests for known font outputs Same pattern as existing DocQA.test.js

Architecture

                     ┌─────────────────────────┐
  Renderer (UI):     │  src/renderer/          │
  - ascii-generator  │   ascii-controller.js   │
    .html            │  - font-picker          │
  - ascii-           │  - input/options        │
    controller.js    │  - preview              │
                     │  - 3 action buttons     │
                     └────────┬────────────────┘
                              │ IPC: ascii:generate, ascii:list-fonts,
                              │      ascii:last-font, ascii:save
                              ▼
                     ┌─────────────────────────┐
  Main (pure):       │  src/main/AsciiArt.js   │
                     │   generate(text, font,  │
                     │            options)     │
                     │   listFonts()           │
                     │   getFontMeta(id)       │
                     └────────┬────────────────┘
                              │
              ┌───────────────┼───────────────┐
              ▼               ▼               ▼
       ┌─────────────┐ ┌──────────────┐ ┌──────────────┐
       │ Hand-coded  │ │   figlet     │ │  Templates   │
       │ (sync)      │ │ (lazy async) │ │ (19 existing)│
       │ 17 fonts    │ │ 400+ fonts   │ │              │
       └─────────────┘ └──────────────┘ └──────────────┘

New Modules

File Role
src/main/AsciiArt.js Pure module. generate({ text, font, options }) returns the ASCII string. listFonts() returns { id, label, kind: 'hand-coded'|'figlet'|'template', sample }[]. getFontMeta(id) returns { kind, height, supportedChars }. Lazy-loads figlet on first figlet:* font request; caches font list.
src/main/AsciiArt.fonts.js Hand-coded font tables (5 existing + 12 new) extracted from renderer.js:6005-6474. Exports HAND_CODED_FONTS as { id → { height, chars: { 'A' → string[height], … } } }.
src/renderer/ascii-controller.js Renderer-side controller. Wires input/options/preview/action-buttons in the standalone window. Calls window.api.ascii.generate(...) etc. (new preload bindings).
tests/ascii-art.test.js Unit tests for AsciiArt.generate() across hand-coded fonts (snapshot), listFonts() shape, getFontMeta(), figlet adapter (mocked), template adapter.
tests/ascii-art.fonts.test.js Per-hand-coded-font known-output snapshot tests for the word "HELLO" (and a few edge cases: empty string, single char, mixed case, digits).
tests/ascii-art.templates.test.js Per-template known-output snapshot for getASCIITemplate('arrow-right', {}) etc.
tests/preload-ascii.test.js Asserts the new ascii:* IPC channels are declared in preload.js allow-list.

Modified Modules

File Change
src/ascii-generator.html Add: (a) searchable font picker dropdown (debounced 100 ms input filter over listFonts()), (b) two new buttons: "Copy to Clipboard" + "Save to File", (c) call window.api.ascii.generate(...) instead of running textToASCII inline. Layout otherwise unchanged.
src/main.js:5584-5611 (openAsciiGenerator) No structural change. The IPC channel name stays open-ascii-generator.
src/main.js (new IPC handlers, after line 5611) Register: ipcMain.handle('ascii:generate', …), ipcMain.handle('ascii:list-fonts', …), ipcMain.handle('ascii:get-font-meta', …), ipcMain.handle('ascii:save', …) (writes user-selected path), ipcMain.handle('ascii:last-font', …) (read + write via electron-store).
src/main.js (require block) Add const AsciiArt = require('./main/AsciiArt') near main.js:4005-4010.
src/preload.js:98-99,294-296 Delete the dead show-ascii-generator and show-ascii-generator-window channels. Add new ascii:generate, ascii:list-fonts, ascii:get-font-meta, ascii:save, ascii:last-font to validInvokeChannels. Add the generators.ascii namespace helpers.
src/renderer.js:2188 Delete the new ModalManager('#ascii-art-dialog') instantiation.
src/renderer.js:5942-6727 Delete the entire in-app ASCII modal block: showASCIIGenerator, hideASCIIGenerator, generateASCIIPreview, insertASCIIArt, createASCIIBox, getASCIITemplate, textToASCII, all 5 font tables, the 19 template strings. (Total ~786 lines.)
src/index.html:814-… (#ascii-art-dialog markup) Delete the entire modal markup block.
src/styles.css + other stylesheets Remove any CSS tied only to the deleted modal.
package.json dependencies Add "figlet": "^1.8.0" (or current latest).
README.md:55-57 (ASCII Art Generator row) Update to mention "17 hand-coded fonts + 400+ FIGlet fonts, copy/save/insert".
README.md:124 (shortcut) No change — Ctrl+Shift+A keeps working (existing standalone window path).
electron-builder.config.js No change (pure-JS dep).

Hand-Coded Fonts (12 new)

Each is a { height, chars: { 'A': [...], 'B': [...], ... '0'..'9', ' ' } } table. Heights vary 4–8 rows.

id label height source inspiration
big Big 8 figlet "Big"
small Small 5 figlet "Small"
lean Lean 6 figlet "Lean"
slant Slant 6 figlet "Slant" (skewed)
isometric1 Isometric 1 6 figlet "Isometric1"
isometric2 Isometric 2 6 figlet "Isometric2"
isometric3 Isometric 3 6 figlet "Isometric3"
isometric4 Isometric 4 6 figlet "Isometric4"
three-d 3-D 7 figlet "3-D"
three-x-five 3x5 5 figlet "3x5"
ansi-shadow ANSI Shadow 8 figlet "ANSI Shadow"
calvin-s Calvin S 7 figlet "Calvin S"

Plus the 5 existing (standard, banner, block, bubble, digital). Total 17 hand-coded.

New UI Features

  1. Searchable font picker. Dropdown with text input. Lists all listFonts() results (17 hand-coded + 400+ figlet = ~417). Fuzzy-match on label, debounced 100 ms. Selecting a font renders a 5-char preview ("HELLO") inline.
  2. Copy to Clipboard button. Copies the rendered ASCII art to the OS clipboard. Uses Electron's clipboard.writeText() (already available via require('electron').clipboard in main process — exposed as window.api.ascii.copy(text) via new preload helper).
  3. Save to File button. Opens a dialog.showSaveDialog with default .txt extension, writes the rendered ASCII art. Returns the saved path on success.
  4. Last-used font memory. New electron-store key ascii:lastFont. On window open, preselect the last-used font.

Data Flow

  1. User triggers Ctrl+Shift+A (existing) or Tools → ASCII Art Generator menu (existing).
  2. main.js:5584 openAsciiGenerator() creates/ focuses the standalone BrowserWindow (existing behaviour).
  3. Renderer loads src/ascii-generator.html. The page calls window.api.ascii.listFonts() on mount, populates the font picker.
  4. User types text, selects font, chooses options (box style, padding) → the preview pane debounces (200 ms) and calls window.api.ascii.generate(...).
  5. Click "Insert": renderer emits existing insert-generated-content IPC → main forwards to mainWindow.webContents.send('insert-content', …) → editor's tabManager.insertAtCursor runs, wrapped in fenced code block.
  6. Click "Copy": renderer calls window.api.ascii.copy(text) → main process writes to clipboard via clipboard.writeText.
  7. Click "Save": renderer calls window.api.ascii.save(text) → main shows save dialog → writes file → returns path. UI shows success toast.

Error Handling

Scenario Handling
figlet fails to lazy-load (dep missing, native binding error) AsciiArt.listFonts() returns hand-coded fonts only; logs a warning. Picker shows "FIGlet fonts unavailable" notice.
figlet font file missing (corrupt install) AsciiArt.generate({ font: 'figlet:Big' }) throws a structured error → controller shows toast + falls back to last working font.
User picks a font with unsupported characters (e.g. non-ASCII) generate() substitutes ? per character; controller shows a one-line warning above the preview.
Clipboard write fails (rare, OS lock) clipboard.writeText returns synchronously and almost never throws; if it does, the controller shows a toast and falls back to "Save to File".
Save dialog cancelled Returns { canceled: true }; UI shows no error.
IPC channel not allowed preload.js is the gate; renderer can only call declared channels. Lint catches new channel declarations missing from allow-list.

Testing

  1. Unit (tests/ascii-art.test.js) — generate({ text: 'HELLO', font: 'big', options: {} }) returns the expected ASCII string (snapshot); listFonts() returns { id, label, kind, sample }[] with hand-coded first; getFontMeta('big') returns { kind: 'hand-coded', height: 8 }.
  2. Snapshot (tests/ascii-art.fonts.test.js) — One snapshot per hand-coded font for generate({ text: 'HELLO', font: '<id>' }). Snapshots are checked into tests/__snapshots__/ascii-art.fonts.test.js.snap. Edge cases: empty string, single char, mixed case, digits.
  3. Snapshot (tests/ascii-art.templates.test.js) — One snapshot per existing template (arrow-right, flowchart, etc.).
  4. Adapter (tests/ascii-art.figlet-adapter.test.js) — figlet.generate is mocked; assert the adapter calls through with correct options, handles the promise, handles errors.
  5. Integration (tests/ascii-art.electron.test.js) — Spawn the actual ascii-generator.html window via electron binary in headless mode and assert the window loads without console errors. Skip if no display available.
  6. Preload (tests/preload-ascii.test.js) — Asserts all new ascii:* channels are in validInvokeChannels allow-list; old show-ascii-generator* channels are gone.

Risks

Risk Likelihood Mitigation
Hand-coded font tables are tedious to author and error-prone Medium Snapshot tests catch drift; visual review for first 4; the rest follow the pattern.
figlet npm package has unexpected transitive deps or native bindings Low figlet is pure JS in v1.x; no native deps. Verify with npm ls figlet after install.
Adding figlet increases bundle size (~400 KB unpacked, ~150 KB gzipped in asar) Low Acceptable: ASCII art is a featured function. Document in THIRD-PARTY-NOTICES.md (MIT license).
Deleting 786 lines from renderer.js might break something unrelated Low The deleted block is self-contained. git grep "showASCIIGenerator|textToASCII|createASCIIBox|getASCIITemplate" after deletion should return only the new module location.
Snapshot tests for fonts become noisy (whitespace, line endings) Medium Normalize line endings to \n; trim trailing whitespace; commit a stable snapshot per font after manual review.
Standalone window cannot share state with the editor (last-used font, theme sync) Low The standalone window is its own BrowserWindow; use electron-store (already used app-wide) for cross-window state. Theme sync is out of scope (standalone window has its own theme via body.theme-<id> from Spec 1).

Acceptance Criteria

  • src/main/AsciiArt.js exists with generate, listFonts, getFontMeta API.
  • 17 hand-coded fonts (5 existing + 12 new) registered, each renders the test word "HELLO" correctly.
  • figlet library loads lazily on first figlet:* font request; lists ~400+ fonts via listFonts().
  • In-app modal (#ascii-art-dialog, showASCIIGenerator*, preload.js:294-296) is fully deleted; git grep returns zero hits for these symbols outside the deletion commit.
  • Standalone window has Copy to Clipboard, Save to File, and Insert buttons. All three work end-to-end.
  • Last-used font is remembered across window opens via electron-store ascii:lastFont.
  • npm test passes with the new test files added.
  • npm run lint clean, npm run format:check clean.
  • npm run build:linux succeeds (no native-binding surprises from figlet).
  • README updated to mention the expanded font catalog and the new output destinations.