Skip to content

Latest commit

 

History

History
1188 lines (1001 loc) · 41.5 KB

File metadata and controls

1188 lines (1001 loc) · 41.5 KB

TUI UX Design and Architecture

Version: 1.0 Last Updated: 2026-01-11 Related ADR: 002-tui-first-interface-design.md


Table of Contents

  1. Design Philosophy
  2. Architecture Overview
  3. Visual Design Language
  4. Screen Layouts
  5. Interaction Patterns
  6. Code Architecture
  7. Component Library
  8. Workflows
  9. Accessibility

Design Philosophy

Core Principles

  1. TUI is home base, commands are express lanes

    • Default behavior launches full TUI
    • Direct commands for automation and quick operations
    • Users should naturally gravitate to TUI for exploration
  2. Progressive disclosure

    • Summary first, details on demand
    • Don't overwhelm with information
    • Drill down for specifics when needed
  3. Immediate visual feedback

    • Every keypress acknowledged
    • State changes visible immediately
    • Loading states for async operations
  4. Information hierarchy

    • Critical errors (red, blocking) → highest priority
    • Warnings (yellow, review) → medium priority
    • Success states (green) → confirmation
    • Contextual help (dimmed) → always available
  5. Escape hatches everywhere

    • q or ESC to go back (always)
    • ? for contextual help (always visible)
    • Ctrl+C emergency exit
    • Never trap users in a state

Architecture Overview

System Architecture

Iris is built on a shared core with three independent interfaces:

┌─────────────────────────────────────────────────┐
│                   src/lib/                      │
│            (Shared Core Logic)                  │
│                                                 │
│   ┌──────────┐ ┌──────────┐ ┌──────────┐        │
│   │parser.ts │ │validator │ │generator │        │
│   │          │ │   .ts    │ │   .ts    │        │
│   └──────────┘ └──────────┘ └──────────┘        │
│                                                 │
│               ┌──────────┐                      │
│               │storage   │                      │
│               │   .ts    │                      │
│               └──────────┘                      │
└─────────────────────────────────────────────────┘
          ▲              ▲               ▲
          │              │               │
          │              │               │
    ┌─────┴────┐   ┌─────┴────┐   ┌──────┴──────┐
    │   TUI    │   │ Commands │   │ Desktop GUI │
    │          │   │          │   │             │
    │ Primary  │   │Automation│   │  Optional   │
    └──────────┘   └──────────┘   └─────────────┘

Core Processing (src/lib/)

The single source of truth - all business logic lives here:

  • parser.ts - CSV parsing with header-based column matching
  • validator.ts - Semantic validation (beyond XML structure)
  • generator.ts - ILR XML generation
  • storage.ts - Cross-submission data persistence

Critical principle: Core is interface-agnostic. Zero knowledge of TUI, commands, or GUI.

Three User-Facing Interfaces

1. TUI (Primary Interface) - src/tui/

  • Full-screen interactive terminal application
  • Default behavior: iris launches TUI
  • Provides rich UI for complex workflows

2. Direct Commands (Automation) - src/commands/

  • Scriptable, non-interactive execution
  • Behavior: iris convert file.csv executes and exits
  • Perfect for CI/CD, shell scripts

3. Desktop GUI (Optional) - src/routes/ + src-tauri/

  • Native desktop app (macOS, Windows, Linux)
  • Tauri + SvelteKit
  • For users who prefer graphical interface

Entry Point: src/cli.ts

Routes execution to appropriate interface:

if (noArgs) {
  // Default: Launch TUI
  const tui = new TUI();
  await tui.start();
}
else if (command && flags.interactive) {
  // Hybrid: TUI for specific workflow
  const tui = new TUI({ startCommand: command });
  await tui.start();
}
else if (command) {
  // Direct command execution
  await commands[command].execute(args);
}

Data Flow Examples

Example 1: CSV Conversion via TUI

User types: iris
  ↓
cli.ts detects no arguments
  ↓
Launches TUI Application
  ↓
Dashboard screen renders
  ↓
User selects "1. Convert CSV to ILR XML"
  ↓
ConvertWorkflow.execute() starts
  ↓
FilePicker screen shows
  ↓
User navigates to ~/Downloads/learners.csv
  ↓
User presses ENTER
  ↓
ProcessingScreen renders with "Parsing CSV..."
  ↓
Workflow calls: parse(csvPath, {
  onProgress: (p) => processingScreen.updateProgress(p),
  onWarning: (w) => processingScreen.addLog(w)
})
  ↓
lib/parser.ts:
  - Reads file
  - Matches headers
  - Parses rows
  - Fires progress callbacks (10%, 20%, 30%...)
  ↓
ProcessingScreen updates:
  - Progress bar: ████░░░░░░ 40%
  - Live log: "Row 42: Missing postcode (optional)"
  ↓
Parse completes, returns { success: true, data: [...] }
  ↓
ProcessingScreen updates: "Generating ILR XML..."
  ↓
Workflow calls: generate(data, {
  onProgress: (p) => processingScreen.updateProgress(p)
})
  ↓
lib/generator.ts:
  - Creates XML structure
  - Writes learner records
  - Fires progress callbacks
  ↓
ProcessingScreen updates:
  - Progress bar: ████████░░ 80%
  ↓
Generate completes, returns { path: '~/.iris/submissions/2026-01.xml' }
  ↓
SuccessScreen renders:
  - ✓ Successfully created ILR XML
  - Output: ~/.iris/submissions/2026-01.xml
  - Size: 2.3 MB
  - Actions: [O]pen, [V]alidate, [R]eturn
  ↓
User presses 'R'
  ↓
Returns to Dashboard

Example 2: CSV Conversion via Direct Command

User types: iris convert ~/Downloads/learners.csv
  ↓
cli.ts detects command + args
  ↓
Routes to commands/convert.ts
  ↓
convert.execute(['~/Downloads/learners.csv']) starts
  ↓
consola.box displays:
  ┌─ Iris CSV → XML Conversion ─┐
  │ Processing: learners.csv     │
  └──────────────────────────────┘
  ↓
Spinner starts: ora('Parsing CSV file...').start()
  ↓
Calls: parse(csvPath, {
  onProgress: (p) => { /* update spinner */ }
})
  ↓
lib/parser.ts processes CSV (same logic as TUI)
  ↓
Spinner updates: ⠸ Parsing CSV file... (67%)
  ↓
Parse completes
  ↓
Spinner.succeed('Parsed 147 learner records')
  ↓
New spinner: ora('Generating ILR XML...').start()
  ↓
Calls: generate(data)
  ↓
lib/generator.ts creates XML (same logic as TUI)
  ↓
Spinner.succeed('Created ~/.iris/submissions/2026-01.xml')
  ↓
consola.success('Conversion complete!')
consola.info('Output: ~/.iris/submissions/2026-01.xml')
consola.info('Size: 2.3 MB')
  ↓
If warnings exist:
consola.warn('3 warnings:')
consola.warn('  • Row 42: Missing postcode')
consola.warn('  • Row 89: Future start date')
  ↓
Process exits with code 0

Example 3: CSV Conversion via Desktop GUI

User opens Iris.app (macOS/Windows/Linux)
  ↓
Tauri launches SvelteKit frontend
  ↓
+page.svelte renders:
  - File picker UI
  - "Select CSV File" button
  ↓
User clicks button
  ↓
SvelteKit calls Tauri API: dialog.open({
  filters: [{ name: 'CSV', extensions: ['csv'] }]
})
  ↓
Native file picker opens (OS-level)
  ↓
User selects ~/Downloads/learners.csv
  ↓
File path returned to SvelteKit
  ↓
Component displays: "Processing learners.csv..."
  ↓
SvelteKit server action triggered
  ↓
Server-side code imports from lib/:
  const result = await parse(csvPath)
  ↓
lib/parser.ts processes CSV (same logic as TUI/commands)
  ↓
Server optionally streams progress to frontend:
  - WebSocket or server-sent events
  - Component updates progress bar
  ↓
Parse completes
  ↓
Server calls: await generate(result.data)
  ↓
lib/generator.ts creates XML (same logic)
  ↓
Server returns result to frontend
  ↓
Component renders success view:
  - ✓ Conversion successful
  - Output location
  - File size
  - "Open in Finder" button (Tauri shell API)
  - "Validate" button
  ↓
User can continue with next operation

Key Architectural Principles

  1. Single Source of Truth: All CSV parsing logic in lib/parser.ts
  2. Identical Logic: Same validation rules across TUI, commands, and GUI
  3. Inversion of Control: Core accepts callbacks (onProgress, onWarning)
  4. Interface Segregation: TUI, commands, GUI never import each other
  5. No Leakage: Core never references terminal, console, or GUI concepts

Why This Matters

Consistency:

  • Same CSV → same XML, regardless of interface
  • Validation errors identical everywhere
  • User trust: behavior is predictable

Testability:

  • Core logic tested independently
  • Mock callbacks in tests
  • No UI dependencies in business logic

Maintainability:

  • Fix parser bug → fixed everywhere
  • Add validation rule → applies to all interfaces
  • Refactor core without touching UI code

Flexibility:

  • Add new interface (web app, API) → just consume lib/
  • Change TUI framework → core unaffected
  • Replace desktop GUI framework → core unaffected

Visual Design Language

Color Palette

export const theme = {
  // Status colors
  success: '#10b981',    // Emerald 500
  warning: '#f59e0b',    // Amber 500
  error: '#ef4444',      // Red 500
  info: '#3b82f6',       // Blue 500

  // UI colors
  primary: '#8b5cf6',    // Violet 500 (brand)
  secondary: '#6366f1',  // Indigo 500
  accent: '#ec4899',     // Pink 500
  highlight: '#14b8a6',  // Teal 500

  // Neutral colors
  text: '#f3f4f6',       // Gray 100 (light text)
  textMuted: '#9ca3af',  // Gray 400 (dimmed text)
  border: '#4b5563',     // Gray 600 (borders)
  background: '#1f2937', // Gray 800 (backgrounds)
};

Typography Hierarchy

Borders:

┏━━━━━━━━━━━━┓  // Heavy double-line borders (main containers)
┃            ┃
┗━━━━━━━━━━━━┛

┌────────────┐  // Light single-line borders (panels, sections)
│            │
└────────────┘

─────────────   // Horizontal dividers

Symbols:

✓  Success indicator
✗  Error indicator (blocking)
⚠  Warning indicator (non-blocking)
→  Selection indicator / navigation arrow
•  List bullets
⋯  Loading / in-progress indicator
█  Progress bar fill
░  Progress bar empty

Text Styles:

BOLD UPPERCASE     // Screen titles
Title Case         // Section headers
Sentence case      // Body text
  Indented         // Nested items
  →  Selected      // Active selection

Layout Grid

All screens follow a consistent grid:

┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃  Header (1-2 lines)              Info  ┃  ← Title bar
┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
┃                                        ┃
┃  Main content area (flexible height)   ┃  ← Primary content
┃                                        ┃
┃  ┌─ Panel 1 ───────────────────────┐   ┃
┃  │                                 │   ┃
┃  └─────────────────────────────────┘   ┃
┃                                        ┃
┃  ┌─ Panel 2 ───────────────────────┐   ┃
┃  │                                 │   ┃
┃  └─────────────────────────────────┘   ┃
┃                                        ┃
┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
┃  [Keys] Help text           Status     ┃  ← Status bar
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛

Screen Layouts

1. Dashboard (Entry Screen)

Purpose: Main menu and recent activity overview

Layout:

┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃  ██╗██████╗ ██╗███████╗                         v0.4.0  ┃
┃  ██║██╔══██╗██║██╔════╝                        Session  ┃
┃  ██║██████╔╝██║███████╗                         Active  ┃
┃  ██║██╔══██╗██║╚════██║                                 ┃
┃  ██║██║  ██║██║███████║    ILR Toolkit                  ┃
┃  ╚═╝╚═╝  ╚═╝╚═╝╚══════╝                                 ┃
┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
┃                                                         ┃
┃  ┌─ Quick Actions ───────────────────────────────────┐  ┃
┃  │                                                   │  ┃
┃  │  → 1  Convert CSV to ILR XML                      │  ┃
┃  │    2  Validate XML Submission                     │  ┃
┃  │    3  Cross-Submission Check                      │  ┃
┃  │    4  Browse Submission History                   │  ┃
┃  │    5  Settings & Configuration                    │  ┃
┃  │                                                   │  ┃
┃  └───────────────────────────────────────────────────┘  ┃
┃                                                         ┃
┃  ┌─ Recent Activity ─────────────────────────────────┐  ┃
┃  │                                                   │  ┃
┃  │  ✓  2026-01-10  learners-jan.csv → 2026-01.xml    │  ┃
┃  │  ✓  2026-01-08  apprentices.csv → 2025-12.xml     │  ┃
┃  │  ⚠  2026-01-05  outcomes.csv (3 warnings)         │  ┃
┃  │                                                   │  ┃
┃  └───────────────────────────────────────────────────┘  ┃
┃                                                         ┃
┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
┃  [1-5] Select  [?] Help  [q] Quit              ~/.iris/ ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛

Interactions:

  • Number keys 1-5 → Launch workflow
  • Arrow keys ↑↓ → Navigate menu (alternative to numbers)
  • ENTER → Confirm selection
  • ? → Show help overlay
  • q → Quit application

2. File Selection Screen

Purpose: Browse and select CSV files

Layout:

┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃  Select CSV File                                 [1/4] ┃
┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
┃                                                        ┃
┃  ┌─ ~/Downloads ────────────────────────────────────┐  ┃
┃  │                                                  │  ┃
┃  │  📁  ..                                          │  ┃
┃  │  📄  apprentices-2024.csv         2.3 MB  Jan 10 │  ┃
┃  │→ 📄  learners-feb-2026.csv        1.8 MB  Jan 09 │  ┃
┃  │  📄  outcomes.csv                 456 KB  Jan 05 │  ┃
┃  │  📄  previous-submission.csv      3.1 MB  Dec 20 │  ┃
┃  │  📁  archive/                                    │  ┃
┃  │                                                  │  ┃
┃  └──────────────────────────────────────────────────┘  ┃
┃                                                        ┃
┃  ┌─ File Preview ───────────────────────────────────┐  ┃
┃  │                                                  │  ┃
┃  │  learners-feb-2026.csv                           │  ┃
┃  │  ─────────────────────────────────────────       │  ┃
┃  │  Size: 1.8 MB                                    │  ┃
┃  │  Modified: 2026-01-09 14:32                      │  ┃
┃  │  Lines: ~147 (estimated)                         │  ┃
┃  │                                                  │  ┃
┃  └──────────────────────────────────────────────────┘  ┃
┃                                                        ┃
┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
┃ [↑↓] Navigate  [ENTER] Select  [TAB] Type path   [ESC] ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛

Interactions:

  • Arrow keys ↑↓ → Navigate file list
  • ENTER → Select file
  • TAB → Switch to path input mode (type path directly)
  • / → Quick search/filter
  • ESC → Cancel, return to dashboard

3. Processing Screen

Purpose: Show live progress during CSV parsing and XML generation

Layout:

┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃  Converting: learners-feb-2026.csv                  [2/4] ┃
┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
┃                                                           ┃
┃  ┌─ Parsing CSV ───────────────────────────────────────┐  ┃
┃  │  ✓  Read file                           1.8 MB      │  ┃
┃  │  ✓  Parse headers                       25 columns  │  ┃
┃  │  ⋯  Parse records                       98/147      │  ┃
┃  │                                                     │  ┃
┃  │  ████████████████████████░░░░░░░  67%               │  ┃
┃  └─────────────────────────────────────────────────────┘  ┃
┃                                                           ┃
┃  ┌─ Live Log ──────────────────────────────────────────┐  ┃
┃  │                                                     │  ┃
┃  │  14:35:02  Matched column: LearnRefNumber           │  ┃
┃  │  14:35:02  Matched column: UKPRN                    │  ┃
┃  │  14:35:03  ⚠ Row 42: Missing postcode (optional)    │  ┃
┃  │  14:35:04  ⚠ Row 89: Future start date - verify     │  ┃
┃  │  14:35:05  Processing learning aims...              │  ┃
┃  │  14:35:05  ⋯ Current: Learner LRN098                │  ┃
┃  │                                                     │  ┃
┃  └─────────────────────────────────────────────────────┘  ┃
┃                                                           ┃
┃  ┌─ Summary ───────────────────────────────────────────┐  ┃
┃  │  Learners: 98/147  │  Aims: 234  │  Warnings: 3     │  ┃
┃  └─────────────────────────────────────────────────────┘  ┃
┃                                                           ┃
┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
┃  Processing...                          Elapsed: 00:03s   ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛

Features:

  • Real-time progress updates
  • Live log scrolling (auto-scrolls to bottom)
  • Multi-stage progress tracking
  • Warnings displayed as they occur

4. Validation Results Screen

Purpose: Explore validation errors and warnings

Layout:

┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃  Validation Results: 2026-01.xml                   [3/4] ┃
┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
┃                                                          ┃
┃  Status: ✗ FAILED                                        ┃
┃                                                          ┃
┃  ┌─ Errors (3 blocking) ──────────────────────────────┐  ┃
┃  │                                                    │  ┃
┃  │→ ✗  Missing UKPRN                    12 records    │  ┃
┃  │  ✗  Invalid ULN format               Learner #45   │  ┃
┃  │  ✗  Overlapping learning aims        Learner #23   │  ┃
┃  │                                                    │  ┃
┃  └────────────────────────────────────────────────────┘  ┃
┃                                                          ┃
┃  ┌─ Warnings (7 non-blocking) ────────────────────────┐  ┃
┃  │                                                    │  ┃
┃  │  ⚠  Missing ethnicity data           18 records    │  ┃
┃  │  ⚠  Missing prior attainment          9 records    │  ┃
┃  │  ⚠  Unusual funding model             Learner #67  │  ┃
┃  │  ...and 4 more                                     │  ┃
┃  │                                                    │  ┃
┃  └────────────────────────────────────────────────────┘  ┃
┃                                                          ┃
┃  ┌─ Error Detail ─────────────────────────────────────┐  ┃
┃  │                                                    │  ┃
┃  │  Field: UKPRN (UK Provider Reference Number)       │  ┃
┃  │  Severity: BLOCKING                                │  ┃
┃  │  Affected: 12 learner records                      │  ┃
┃  │                                                    │  ┃
┃  │  This mandatory field is missing. ESFA submissions │  ┃
┃  │  will be rejected without valid UKPRN values.      │  ┃
┃  │                                                    │  ┃
┃  │  Affected learners:                                │  ┃
┃  │  LRN001, LRN007, LRN023, LRN034, LRN056...         │  ┃
┃  │                                                    │  ┃
┃  └────────────────────────────────────────────────────┘  ┃
┃                                                          ┃
┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
┃  [↑↓] Navigate  [ENTER] Details  [E] Export  [ESC] Back  ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛

Interactions:

  • Arrow keys ↑↓ → Navigate errors/warnings list
  • ENTER → Show full error details
  • E → Export error list to CSV
  • TAB → Toggle between errors and warnings
  • ESC → Return to dashboard

5. Success/Completion Screen

Purpose: Confirm successful operation and offer next actions

Layout:

┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃  Conversion Complete                                [4/4] ┃
┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
┃                                                           ┃
┃                          ✓                                ┃
┃                                                           ┃
┃              Successfully created ILR XML                 ┃
┃                                                           ┃
┃  ┌─ Output ────────────────────────────────────────────┐  ┃
┃  │                                                     │  ┃
┃  │  File: ~/.iris/submissions/2026-02.xml              │  ┃
┃  │  Size: 2.3 MB                                       │  ┃
┃  │  Records: 147 learners, 342 aims                    │  ┃
┃  │  Warnings: 3 (non-blocking)                         │  ┃
┃  │                                                     │  ┃
┃  └─────────────────────────────────────────────────────┘  ┃
┃                                                           ┃
┃  ┌─ Next Actions ──────────────────────────────────────┐  ┃
┃  │                                                     │  ┃
┃  │  → O  Open in editor                                │  ┃
┃  │    V  Validate now                                  │  ┃
┃  │    C  Copy path to clipboard                        │  ┃
┃  │    R  Return to main menu                           │  ┃
┃  │                                                     │  ┃
┃  └─────────────────────────────────────────────────────┘  ┃
┃                                                           ┃
┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
┃  [O/V/C/R] Select action  [ESC] Back to menu              ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛

Interaction Patterns

Keyboard Navigation

Global Shortcuts:

  • q or ESC → Go back / quit (context-dependent)
  • Ctrl+C → Emergency exit (always)
  • ? → Help overlay (always available)
  • Ctrl+L → Refresh screen

List Navigation:

  • / k → Move up
  • / j → Move down
  • g → Go to top (vim-style)
  • G → Go to bottom (vim-style)
  • ENTER → Select / confirm
  • / → Search / filter

Tabbed Panels:

  • TAB → Next panel
  • Shift+TAB → Previous panel

Visual Feedback

Keypress Acknowledgment:

// Brief flash on selection
term.saveCursor();
term.bgBrightCyan(' Selected Item ');
setTimeout(() => {
  term.restoreCursor();
  term(' Selected Item ');
}, 100);

Loading States:

// Spinner for indeterminate progress
const spinner = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];

// Progress bar for determinate progress
████████████░░░░░░░░  60%

State Transitions:

// Fade out old screen, fade in new screen
// Slide panels in from right
// Pulse effect for warnings (cycle through amber shades)

Code Architecture

Directory Structure

src/
├── tui/
│   ├── app.ts                 # Main TUI application
│   ├── theme.ts               # Colors, borders, symbols
│   │
│   ├── screens/               # Full-screen views
│   │   ├── dashboard.ts       # Main menu
│   │   ├── file-picker.ts     # File selection
│   │   ├── processing.ts      # Progress screen
│   │   ├── validation.ts      # Validation results
│   │   └── success.ts         # Completion screen
│   │
│   ├── components/            # Reusable UI components
│   │   ├── panel.ts           # Bordered panel
│   │   ├── menu.ts            # Interactive menu
│   │   ├── progress-bar.ts    # Progress indicator
│   │   ├── table.ts           # Data table
│   │   └── log-viewer.ts      # Scrolling log
│   │
│   ├── workflows/             # Multi-step processes
│   │   ├── convert.ts         # CSV → XML workflow
│   │   ├── validate.ts        # Validation workflow
│   │   └── check.ts           # Cross-submission check
│   │
│   └── utils/                 # TUI utilities
│       ├── keyboard.ts        # Keyboard handling
│       ├── layout.ts          # Layout calculations
│       └── animations.ts      # Transition effects
│
├── commands/                  # Direct command implementations
│   ├── convert.ts             # Non-TUI convert
│   ├── validate.ts            # Non-TUI validate
│   └── check.ts               # Non-TUI check
│
├── lib/                       # Shared core (unchanged)
│   ├── parser.ts
│   ├── validator.ts
│   ├── generator.ts
│   └── storage.ts
│
└── cli.ts                     # Entry point (router)

Application Entry Point

// src/cli.ts
#!/usr/bin/env bun

import { parseArgs } from 'util';
import { TUI } from './tui/app';
import { commands } from './commands';
import consola from 'consola';

const { values, positionals } = parseArgs({
  allowPositionals: true,
  options: {
    help: { type: 'boolean', short: 'h' },
    version: { type: 'boolean', short: 'v' },
    interactive: { type: 'boolean', short: 'i' },
  },
});

// No command = Launch TUI
if (positionals.length === 0 && !values.version && !values.help) {
  const tui = new TUI();
  await tui.start();
  process.exit(0);
}

// Handle --version, --help
if (values.version) {
  consola.info('iris v0.4.0');
  process.exit(0);
}

if (values.help) {
  showHelp();
  process.exit(0);
}

// Execute direct command
const command = positionals[0];
const args = positionals.slice(1);

if (values.interactive) {
  // Launch TUI but jump to specific workflow
  const tui = new TUI({ startCommand: command, args });
  await tui.start();
} else {
  // Direct command execution with pretty output
  await commands[command]?.execute(args);
}

TUI Application Class

// src/tui/app.ts
import terminalKit from 'terminal-kit';
import { Dashboard } from './screens/dashboard';
import { ConvertWorkflow } from './workflows/convert';
import { ValidateWorkflow } from './workflows/validate';
import { theme } from './theme';

const term = terminalKit.terminal;

export class TUI {
  private currentWorkflow: any = null;

  constructor(private options: { startCommand?: string; args?: string[] } = {}) {}

  async start() {
    this.initialize();

    if (this.options.startCommand) {
      // Jump directly to workflow
      await this.launchWorkflow(this.options.startCommand);
    } else {
      // Show dashboard
      await this.showDashboard();
    }
  }

  private initialize() {
    // Full-screen mode
    term.fullscreen(true);
    term.hideCursor();
    term.grabInput({ mouse: 'button' });

    // Graceful shutdown
    term.on('key', (key) => {
      if (key === 'CTRL_C') {
        this.cleanup();
        process.exit(0);
      }
    });

    // Handle window resize
    process.stdout.on('resize', () => {
      this.refresh();
    });
  }

  async showDashboard() {
    const dashboard = new Dashboard(term);
    const selection = await dashboard.render();

    await this.launchWorkflow(selection);
  }

  async launchWorkflow(command: string) {
    switch (command) {
      case 'convert':
        this.currentWorkflow = new ConvertWorkflow(term);
        break;
      case 'validate':
        this.currentWorkflow = new ValidateWorkflow(term);
        break;
      case 'check':
        // ... other workflows
        break;
      case 'quit':
        this.cleanup();
        process.exit(0);
        return;
    }

    const result = await this.currentWorkflow.execute();

    // Return to dashboard after workflow completes
    await this.showDashboard();
  }

  private cleanup() {
    term.fullscreen(false);
    term.showCursor();
    term.grabInput(false);
  }

  private refresh() {
    // Re-render current screen on terminal resize
    if (this.currentWorkflow) {
      this.currentWorkflow.render();
    }
  }
}

Component Library

Panel Component

// src/tui/components/panel.ts
import terminalKit from 'terminal-kit';
import { theme } from '../theme';

export class Panel {
  constructor(
    private term: any,
    private options: {
      title?: string;
      x: number;
      y: number;
      width: number;
      height: number;
      style?: 'single' | 'double';
    }
  ) {}

  render(content: string[]) {
    const { x, y, width, height, title, style = 'single' } = this.options;

    // Draw box
    this.term.drawBox({
      x, y, width, height,
      style: style === 'double' ? 'heavy' : 'light',
      title: title ? ` ${title} ` : undefined,
    });

    // Render content inside box
    content.forEach((line, i) => {
      if (i < height - 2) {
        this.term.moveTo(x + 2, y + i + 1);
        this.term(line.slice(0, width - 4));
      }
    });
  }
}

Progress Bar Component

// src/tui/components/progress-bar.ts
export class ProgressBar {
  constructor(
    private term: any,
    private options: {
      x: number;
      y: number;
      width: number;
    }
  ) {}

  update(progress: number) {
    const { x, y, width } = this.options;
    const filled = Math.floor((progress / 100) * width);
    const empty = width - filled;

    this.term.moveTo(x, y);
    this.term.bgGreen(' '.repeat(filled));
    this.term.bgGray(' '.repeat(empty));
    this.term.styleReset();
    this.term(` ${progress}%`);
  }
}

Workflows

Convert Workflow

// src/tui/workflows/convert.ts
import { parse } from '../../lib/parser';
import { generate } from '../../lib/generator';
import { FilePicker } from '../screens/file-picker';
import { ProcessingScreen } from '../screens/processing';
import { SuccessScreen } from '../screens/success';

export class ConvertWorkflow {
  constructor(private term: any) {}

  async execute() {
    // Step 1: Select file
    const filePicker = new FilePicker(this.term);
    const csvPath = await filePicker.show();

    if (!csvPath) {
      return { cancelled: true };
    }

    // Step 2: Process with live updates
    const processingScreen = new ProcessingScreen(this.term);
    processingScreen.show('Converting CSV to XML');

    const parseResult = await parse(csvPath, {
      onProgress: (p) => processingScreen.updateProgress(p),
      onWarning: (w) => processingScreen.addLog(w),
    });

    if (!parseResult.success) {
      // Show error screen
      return { success: false, errors: parseResult.errors };
    }

    const xmlResult = await generate(parseResult.data, {
      onProgress: (p) => processingScreen.updateProgress(p),
    });

    // Step 3: Show success
    const successScreen = new SuccessScreen(this.term);
    const action = await successScreen.show(xmlResult);

    return { success: true, action };
  }
}

Accessibility

Terminal Compatibility

Minimum Requirements:

  • 256-color support
  • Unicode/UTF-8 support
  • Terminal width: 80 columns minimum, 120 recommended
  • Terminal height: 24 rows minimum

Tested Terminals:

  • macOS Terminal.app
  • iTerm2
  • Alacritty
  • VS Code integrated terminal
  • Warp

Graceful Degradation:

  • Detect color support, fall back to 16 colors if needed
  • Detect Unicode support, fall back to ASCII box drawing
  • Responsive layout adapts to terminal size

Keyboard-Only Navigation

All functionality accessible without mouse:

  • Arrow keys for navigation
  • Vim-style hjkl alternative
  • Tab for panel switching
  • Single-key shortcuts for common actions
  • Always-visible keyboard hints

Screen Reader Compatibility

While TUIs have inherent screen reader challenges:

  • Use semantic text where possible
  • Provide text alternatives for symbols
  • Clear status announcements for state changes

Animation Examples

Screen Transitions

// Fade out old screen
for (let opacity = 100; opacity >= 0; opacity -= 10) {
  term.colorGrayscale(opacity / 100);
  // Re-render screen
  await sleep(20);
}

// Fade in new screen
for (let opacity = 0; opacity <= 100; opacity += 10) {
  term.colorGrayscale(opacity / 100);
  // Render new screen
  await sleep(20);
}

Spinner Animation

const spinners = {
  dots: ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'],
  arrow: ['←', '↖', '↑', '↗', '→', '↘', '↓', '↙'],
};

let frame = 0;
const interval = setInterval(() => {
  term.moveTo(x, y);
  term(spinners.dots[frame % spinners.dots.length]);
  frame++;
}, 80);

Pulse Effect

// Pulse warning symbol
const colors = ['#f59e0b', '#fbbf24', '#fcd34d', '#fbbf24'];
let i = 0;

setInterval(() => {
  term.moveTo(x, y);
  term.colorRgbHex(colors[i % colors.length]);
  term('⚠');
  i++;
}, 300);

Testing Strategy

Unit Tests

Test pure functions in isolation:

  • Layout calculations
  • Theme utilities
  • Keyboard event parsing

Integration Tests

Test workflows without terminal output:

  • Mock terminal interface
  • Test workflow state machines
  • Verify correct screen transitions

Manual Testing

Use test fixtures:

  • Sample CSV files (valid, invalid, edge cases)
  • Pre-generated XML files
  • Simulated user input sequences

Performance Considerations

  1. Minimize redraws: Only redraw changed regions
  2. Debounce rapid updates: Aggregate log messages
  3. Lazy rendering: Don't render off-screen content
  4. Efficient diffing: Track what changed, redraw only deltas
  5. Progress throttling: Update progress max 10x/second

Future Enhancements

  1. Mouse support: Click to select menu items
  2. Split panes: Multiple simultaneous views
  3. Tabs: Switch between multiple files
  4. Search: Fuzzy search in error lists
  5. Themes: Dark/light mode, custom color schemes
  6. Macros: Record and replay action sequences
  7. Plugins: Extensible command system

References