Version: 1.0 Last Updated: 2026-01-11 Related ADR: 002-tui-first-interface-design.md
- Design Philosophy
- Architecture Overview
- Visual Design Language
- Screen Layouts
- Interaction Patterns
- Code Architecture
- Component Library
- Workflows
- Accessibility
-
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
-
Progressive disclosure
- Summary first, details on demand
- Don't overwhelm with information
- Drill down for specifics when needed
-
Immediate visual feedback
- Every keypress acknowledged
- State changes visible immediately
- Loading states for async operations
-
Information hierarchy
- Critical errors (red, blocking) → highest priority
- Warnings (yellow, review) → medium priority
- Success states (green) → confirmation
- Contextual help (dimmed) → always available
-
Escape hatches everywhere
qorESCto go back (always)?for contextual help (always visible)Ctrl+Cemergency exit- Never trap users in a state
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 │
└──────────┘ └──────────┘ └─────────────┘
The single source of truth - all business logic lives here:
parser.ts- CSV parsing with header-based column matchingvalidator.ts- Semantic validation (beyond XML structure)generator.ts- ILR XML generationstorage.ts- Cross-submission data persistence
Critical principle: Core is interface-agnostic. Zero knowledge of TUI, commands, or GUI.
1. TUI (Primary Interface) - src/tui/
- Full-screen interactive terminal application
- Default behavior:
irislaunches TUI - Provides rich UI for complex workflows
2. Direct Commands (Automation) - src/commands/
- Scriptable, non-interactive execution
- Behavior:
iris convert file.csvexecutes 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
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);
}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
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
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
- Single Source of Truth: All CSV parsing logic in
lib/parser.ts - Identical Logic: Same validation rules across TUI, commands, and GUI
- Inversion of Control: Core accepts callbacks (
onProgress,onWarning) - Interface Segregation: TUI, commands, GUI never import each other
- No Leakage: Core never references terminal, console, or GUI concepts
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
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)
};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
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
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
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 overlayq→ Quit application
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 fileTAB→ Switch to path input mode (type path directly)/→ Quick search/filterESC→ Cancel, return to dashboard
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
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 detailsE→ Export error list to CSVTAB→ Toggle between errors and warningsESC→ Return to dashboard
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 ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
Global Shortcuts:
qorESC→ 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 downg→ Go to top (vim-style)G→ Go to bottom (vim-style)ENTER→ Select / confirm/→ Search / filter
Tabbed Panels:
TAB→ Next panelShift+TAB→ Previous panel
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)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)
// 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);
}// 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();
}
}
}// 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));
}
});
}
}// 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}%`);
}
}// 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 };
}
}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
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
While TUIs have inherent screen reader challenges:
- Use semantic text where possible
- Provide text alternatives for symbols
- Clear status announcements for state changes
// 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);
}const spinners = {
dots: ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'],
arrow: ['←', '↖', '↑', '↗', '→', '↘', '↓', '↙'],
};
let frame = 0;
const interval = setInterval(() => {
term.moveTo(x, y);
term(spinners.dots[frame % spinners.dots.length]);
frame++;
}, 80);// 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);Test pure functions in isolation:
- Layout calculations
- Theme utilities
- Keyboard event parsing
Test workflows without terminal output:
- Mock terminal interface
- Test workflow state machines
- Verify correct screen transitions
Use test fixtures:
- Sample CSV files (valid, invalid, edge cases)
- Pre-generated XML files
- Simulated user input sequences
- Minimize redraws: Only redraw changed regions
- Debounce rapid updates: Aggregate log messages
- Lazy rendering: Don't render off-screen content
- Efficient diffing: Track what changed, redraw only deltas
- Progress throttling: Update progress max 10x/second
- Mouse support: Click to select menu items
- Split panes: Multiple simultaneous views
- Tabs: Switch between multiple files
- Search: Fuzzy search in error lists
- Themes: Dark/light mode, custom color schemes
- Macros: Record and replay action sequences
- Plugins: Extensible command system
- terminal-kit Documentation
- consola Documentation
- Awesome TUIs
- lazygit Source - Reference implementation
- The Art of CLI Design