@averagejoeslab/tui
v1.0.1
Published
Terminal user interface framework based on the Elm Architecture
Maintainers
Readme
@puppuccino/tui
Terminal UI framework using the Elm Architecture.
Installation
npm install @puppuccino/tuiOr install from GitHub:
npm install github:averagejoeslab/tuiFeatures
- Elm Architecture - Model-Update-View pattern for predictable state management
- Message-Based Updates - Type-safe messages drive all state changes
- Command System - Handle async operations cleanly
- Built-in Messages - Key, mouse, window resize, focus, paste events
- Input Handling - Full keyboard support with modifier keys
- Terminal Features - Alternate screen, mouse mode, bracketed paste
Quick Start
import { run, Model, Cmd, Key, KeyMsg, isKeyMsg, quit } from '@puppuccino/tui';
// Define your message types
type Msg = KeyMsg | { type: 'tick' };
// Define your model
class Counter implements Model<Msg> {
constructor(private count: number = 0) {}
init(): Cmd<Msg> {
return null; // No initial command
}
update(msg: Msg): [Model<Msg>, Cmd<Msg>] {
if (isKeyMsg(msg)) {
if (msg.key === 'q' || msg.isCtrl('c')) {
return [this, quit()];
}
if (msg.key === Key.Up) {
return [new Counter(this.count + 1), null];
}
if (msg.key === Key.Down) {
return [new Counter(this.count - 1), null];
}
}
return [this, null];
}
view(): string {
return `Count: ${this.count}\n\n↑/↓ to change, q to quit`;
}
}
// Run the program
run(new Counter());Core Concepts
Model
The Model interface defines your application's state and behavior:
interface Model<M extends Msg> {
init(): Cmd<M>; // Initial command
update(msg: M): [Model<M>, Cmd<M>]; // State transition
view(): string; // Render to string
}Messages
Messages are events that trigger state updates:
import {
KeyMsg, // Keyboard input
MouseMsg, // Mouse events
WindowSizeMsg, // Terminal resize
FocusMsg, // Focus gained/lost
PasteMsg, // Pasted text
TickMsg, // Timer tick
CustomMsg, // User-defined messages
} from '@puppuccino/tui';
// Check message types
if (isKeyMsg(msg)) {
console.log(`Key pressed: ${msg.key}`);
console.log(`Modifiers: ctrl=${msg.ctrl} alt=${msg.alt} shift=${msg.shift}`);
}Commands
Commands handle side effects and async operations:
import {
quit, // Exit the program
none, // Do nothing
tick, // Timer that sends a message
tickMsg, // Timer with TickMsg
timeout, // One-shot timer
exec, // Execute async function
batch, // Run multiple commands
} from '@puppuccino/tui';
// Timer example
update(msg: Msg): [Model<Msg>, Cmd<Msg>] {
if (msg.type === 'start') {
return [this, tickMsg(1000, 'timer-id')];
}
return [this, null];
}
// Async operation
const loadData: Cmd<DataMsg> = exec(
() => fetch('/api/data').then(r => r.json()),
(data) => ({ type: 'dataLoaded', data }),
(error) => ({ type: 'error', error })
);Built-in Messages
KeyMsg
import { KeyMsg, Key, isKeyMsg } from '@puppuccino/tui';
// In your update function
if (isKeyMsg(msg)) {
// Check specific keys
if (msg.key === Key.Enter) { /* ... */ }
if (msg.key === Key.Escape) { /* ... */ }
if (msg.key === Key.Up) { /* ... */ }
// Check with modifiers
if (msg.isCtrl('c')) { /* Ctrl+C */ }
if (msg.isAlt('x')) { /* Alt+X */ }
// Check modifiers directly
if (msg.ctrl && msg.key === 's') { /* Ctrl+S */ }
}Available keys:
- Control:
Enter,Tab,Backspace,Escape,Space,Delete - Arrows:
Up,Down,Left,Right - Navigation:
Home,End,PageUp,PageDown,Insert - Function:
F1-F12
WindowSizeMsg
import { WindowSizeMsg, isWindowSizeMsg } from '@puppuccino/tui';
if (isWindowSizeMsg(msg)) {
console.log(`Terminal size: ${msg.width}x${msg.height}`);
}PasteMsg
import { PasteMsg, isPasteMsg } from '@puppuccino/tui';
if (isPasteMsg(msg)) {
console.log(`Pasted text: ${msg.text}`);
}Program Options
import { run, ProgramOptions } from '@puppuccino/tui';
const options: ProgramOptions = {
altScreen: true, // Use alternate screen buffer
mouse: true, // Enable mouse support
bracketedPaste: true, // Enable bracketed paste mode
reportFocus: true, // Report focus changes
fps: 60, // Render frame rate
title: 'My App', // Window title
};
run(model, options);Advanced Usage
Custom Messages
import { CustomMsg, isCustomMsg } from '@puppuccino/tui';
// Send custom messages
const cmd = send('userLoaded', { id: 1, name: 'Alice' });
// Check for custom messages
if (isCustomMsg<User>(msg, 'userLoaded')) {
console.log(msg.payload.name); // Type-safe access
}Batch Commands
import { batch, tick, exec } from '@puppuccino/tui';
// Run multiple commands together
const cmd = batch(
tick(100, () => ({ type: 'tick' })),
exec(loadData, onSuccess, onError)
);Mapping Messages
import { map } from '@puppuccino/tui';
// Transform a command's message type
const mappedCmd = map(
childCmd,
(childMsg) => ({ type: 'childMessage', msg: childMsg })
);Interval Timer
import { every } from '@puppuccino/tui';
// Create a repeating timer
const [cmd, cancel] = every(1000, (time) => ({ type: 'tick', time }));
// Later, cancel the timer
cancel();Complete Example
import {
run,
Model,
Cmd,
Key,
KeyMsg,
TickMsg,
isKeyMsg,
isTickMsg,
tickMsg,
batch,
quit,
} from '@puppuccino/tui';
type Msg = KeyMsg | TickMsg;
class App implements Model<Msg> {
constructor(
private time: Date = new Date(),
private running: boolean = true
) {}
init(): Cmd<Msg> {
return tickMsg(1000, 'clock');
}
update(msg: Msg): [Model<Msg>, Cmd<Msg>] {
if (isKeyMsg(msg)) {
if (msg.key === 'q') return [this, quit()];
if (msg.key === ' ') {
return [new App(this.time, !this.running), null];
}
}
if (isTickMsg(msg) && msg.id === 'clock') {
const nextCmd = this.running ? tickMsg(1000, 'clock') : null;
return [new App(new Date(), this.running), nextCmd];
}
return [this, null];
}
view(): string {
const status = this.running ? '▶ Running' : '⏸ Paused';
return `
╭─────────────────────────╮
│ ${this.time.toLocaleTimeString().padEnd(19)} │
│ ${status.padEnd(21)} │
╰─────────────────────────╯
Space: pause/resume Q: quit
`;
}
}
run(new App(), { altScreen: true });API Reference
Core
run(model, options?)- Run a TUI programcreate(model, options?)- Create program without runningProgram- Program class withrun(),send(),quit()methods
Types
Model<M>- Model interfaceCmd<M>- Command typeMsg- Base message typeProgramOptions- Configuration options
Messages
QuitMsg- Quit the programKeyMsg- Keyboard inputWindowSizeMsg- Terminal resizeFocusMsg- Focus changeMouseMsg- Mouse eventPasteMsg- Pasted textTickMsg- Timer tickErrorMsg- Error occurredCustomMsg<T>- Custom message
Commands
quit()- Exit programnone()- No operationtick(ms, fn)- TimertickMsg(ms, id?)- Timer with TickMsgtimeout(ms, msg)- One-shot timerevery(ms, fn)- Repeating timerexec(fn, success, error?)- Async operationsend(tag, payload)- Send custom messagebatch(...cmds)- Combine commandsmap(cmd, fn)- Transform message
Keys
Key- Key name constantsparseKey(data)- Parse key from inputmatches(key, name, modifiers?)- Match key
License
MIT
