blecsd-tui
npx skills add https://github.com/kadajett/blecsd-skill --skill blecsd-tui
Agent 安装分布
Skill 文档
blECSd Core Library Skill
blECSd is a modern, high-performance terminal UI library built on TypeScript and ECS (Entity Component System) architecture using bitecs. It is a ground-up rewrite of the original blessed node library, NOT backwards-compatible. Version: 0.6.0. Node.js >= 22.0.0.
Hard Rules (Non-Negotiable)
1. Purely Functional, No OOP
BANNED: class, this, new (except Map/Set/Error), prototype manipulation, inheritance.
// WRONG
class MyWidget {
private x: number;
constructor(x: number) { this.x = x; }
move(dx: number) { this.x += dx; }
}
// CORRECT
interface MyWidget { readonly x: number; }
function createMyWidget(x: number): MyWidget { return { x }; }
function moveWidget(w: MyWidget, dx: number): MyWidget { return { x: w.x + dx }; }
2. No Direct bitecs Imports
Only three files may import from 'bitecs': src/core/ecs.ts, src/core/world.ts, src/core/types.ts. Everything else imports from 'blecsd' (external) or '../core/ecs' (internal).
// BANNED
import { addEntity, hasComponent } from 'bitecs';
// CORRECT (external consumer or example)
import { addEntity, hasComponent } from 'blecsd';
// CORRECT (internal library code)
import { addEntity, hasComponent } from '../core/ecs';
3. Library-First Design
Users control their own world and update loop. All functions take world as a parameter. Never own a global world. Never create implicit dependencies on the update loop.
// Users can run systems standalone without the built-in loop
import { createWorld, addEntity, setPosition, renderSystem } from 'blecsd';
const world = createWorld();
const eid = addEntity(world);
setPosition(world, eid, 10, 5);
renderSystem(world); // Works without a scheduler
4. Input Priority
INPUT phase is always first in the update loop. It cannot be reordered. All pending input is processed every frame. No events may be lost or delayed.
5. Early Returns and Guard Clauses
Handle errors first, happy path last. Max nesting 2-3 levels.
// WRONG: deep nesting
function process(world: World, eid: Entity): Result {
if (hasComponent(world, Position, eid)) {
if (hasComponent(world, Velocity, eid)) {
return doWork(world, eid);
}
}
return { error: 'missing' };
}
// CORRECT: guard clauses
function process(world: World, eid: Entity): Result {
if (!hasComponent(world, Position, eid)) return { error: 'no position' };
if (!hasComponent(world, Velocity, eid)) return { error: 'no velocity' };
return doWork(world, eid);
}
6. File Size Limits
- Component files: max 200 lines
- Widget files: max 300 lines per sub-file
- All other source files: max 500 lines
7. Strict TypeScript
- All functions have explicit return types
- No
any(useunknown+ type guards) - Prefer
readonlyarrays and objects - Branded types for IDs
Architecture
Update Loop Phases (in order)
- INPUT (always first, immutable position) – Process all pending keyboard/mouse input
- EARLY_UPDATE – Pre-processing, state transitions
- UPDATE – Main game/app logic
- LATE_UPDATE – Post-processing, cleanup
- ANIMATION – Physics, springs, tweens, momentum scrolling
- LAYOUT – Calculate positions, sizes, constraints
- RENDER – Write to screen buffer
- POST_RENDER – Cleanup, telemetry
Where Does Logic Go?
| Question | Module |
|---|---|
| Pure data storage (typed arrays, stores)? | components/ (200 lines max) |
| Queries entities and transforms state? | systems/ |
| Combines components into user-facing API? | widgets/ (300 lines/sub-file) |
| Pure function, no ECS dependency? | utils/ |
| Validates config or input? | schemas/ |
| Handles terminal I/O? | terminal/ |
| ECS primitive (addEntity, etc.)? | core/ |
Rule: Components = data only. Systems = logic. Never put business logic in component files.
ECS Pattern
Components are pure data containers:
const Position = defineComponent({ x: Types.f32, y: Types.f32 });
Systems are pure functions that query and transform:
const movementSystem = defineSystem((world) => {
const entities = movingQuery(world);
for (const eid of entities) {
Position.x[eid] += Velocity.x[eid];
Position.y[eid] += Velocity.y[eid];
}
return world;
});
Zod Validation
Use Zod at system boundaries only:
const ScreenConfigSchema = z.object({
width: z.number().int().positive(),
height: z.number().int().positive(),
title: z.string().optional(),
});
API Surface (Three Tiers)
Tier 1: Curated Top-Level ('blecsd')
~80 exports for common use cases:
ECS Core:
createWorld(),destroyWorld(world)addEntity(world),removeEntity(world, eid)addComponent(world, eid, Component),hasComponent(world, Component, eid)- Types:
Entity,World,System
Entity Factories:
createScreenEntity(world, config)– Root screen entitycreateBoxEntity(world, config)– Box/containercreateTextEntity(world, config)– Text displaycreateButtonEntity(world, config)– ButtoncreateCheckboxEntity(world, config)– CheckboxcreateInputEntity(world, config)– Text inputcreateSelectEntity(world, config)– Dropdown selectcreateListEntity(world, config)– List
Systems:
inputSystem(world)– Process keyboard/mouselayoutSystem(world)– Calculate layoutrenderSystem(world)– Render to bufferoutputSystem(world)– Write buffer to terminalanimationSystem(world)– Update animationsfocusSystem(world)– Manage focuscleanup(),clearScreen()
Component Helpers:
getText(world, eid),setText(world, eid, text)getPosition(world, eid),setPosition(world, eid, x, y)getDimensions(world, eid),setDimensions(world, eid, w, h)setZIndex(world, eid, z),getZIndex(world, eid),normalizeZIndices(world)toggle(world, eid)– Toggle visibilityscrollByLines(world, eid, delta),scrollToTop/Bottom/Line(world, eid, ...)focusNext(world),focusPrev(world)prepend(world, parentEid, childEid)– Add child to fronthitTest(world, x, y)– Find entity at coordinates
Terminal I/O:
enableInput(world),disableInput(world)enableKeys(world),disableKeys(world)enableMouse(world),disableMouse(world)stripAnsi(str),createDoubleBuffer(w, h)getCell(buf, x, y),setCell(buf, x, y, cell)clearBuffer(buf),fillRect(buf, x, y, w, h, cell)
Schemas:
BoxConfigSchema,PositionValueSchema,TextConfigSchema
Utilities:
renderText(text, options),wrapText(text, width)getLine(rope, n),getLines(rope),getStats(rope)
Tier 2: Module Subpath Imports
Full module access for advanced use:
import { position, content, scroll } from 'blecsd/components';
import { animationSystem, collisionSystem } from 'blecsd/systems';
import { box, tabs, modal, flexbox } from 'blecsd/widgets';
import { createDoubleBuffer } from 'blecsd/terminal';
import { BoxConfigSchema } from 'blecsd/schemas';
import { renderText, wrapText } from 'blecsd/utils';
import { enableDebugOverlay } from 'blecsd/debug';
import { queueKeyEvent } from 'blecsd/input';
Tier 3: Namespace Objects (Preferred for Complex Apps)
Frozen plain objects grouping related functions:
import { position, content, dimensions, border } from 'blecsd/components';
position.set(world, eid, 10, 5);
content.set(world, eid, 'Hello');
dimensions.set(world, eid, 40, 10);
border.set(world, eid, { type: 'line' });
Component Namespaces (from blecsd/components)
Each is a frozen object of pure functions:
| Namespace | Purpose | Key Functions |
|---|---|---|
position |
x, y, z coordinates | set, get, setZIndex, getZIndex |
dimensions |
width, height | set, get |
content |
Text content | set, get, setText, getText |
renderable |
Visibility, style | toggle, setVisible, isVisible |
border |
Border style | set, get, hasBorder |
padding |
Inner padding | set, get |
hierarchy |
Parent-child tree | setParent, getParent, getChildren, prepend |
focus |
Focus management | setFocusable, isFocused |
scroll |
Scroll state | set, get, scrollBy |
interactive |
Click/hover | set, isInteractive |
animation |
Animation state | set, get |
velocity |
Movement speed | set, get |
collision |
Hit detection | set, get, isEnabled |
health |
Entity health/HP | set, get, damage, heal |
sprite |
Sprite data | set, get, setFrame |
tileMap |
Tile grid data | set, get |
camera |
Camera viewport | set, get, setTarget |
particle |
Particle emitter | set, get |
stateMachine |
FSM state | set, get, transition |
timer |
Countdown/stopwatch | set, get, start, stop |
userData |
Custom data store | set, get |
Widget Namespaces (from blecsd/widgets)
Over 50 widget namespaces. Key ones:
| Namespace | Purpose | Create Function |
|---|---|---|
box |
Container with border, padding | createBox(world, config) |
text |
Static text display | createText(world, config) |
buttonWidget |
Clickable button | createButton(world, config) |
checkboxWidget |
Toggle checkbox | createCheckbox(world, config) |
radioWidget |
Radio button group | createRadioGroup(world, config) |
listWidget |
Selectable list | createList(world, config) |
listTable |
Tabular list | createListTable(world, config) |
tabs |
Tab container | createTabs(world, config) |
modal |
Modal dialog | createModal(world, config) |
flexbox |
Flex layout | createFlexContainer(world, config) |
grid |
Grid layout | createGrid(world, config) |
splitPane |
Resizable split view | createSplitPane(world, config) |
scrollableBox |
Scrollable container | createScrollableBox(world, config) |
fileManager |
File browser | createFileManager(world, config) |
tree |
Tree view | createTree(world, config) |
panel |
Titled panel | createPanel(world, config) |
progressBarWidget |
Progress indicator | createProgressBar(world, config) |
formWidget |
Form with fields | createForm(world, config) |
toast |
Toast notification | showInfoToast/showErrorToast/... |
message |
Status message | showInfo/showError/showWarning/showSuccess |
commandPalette |
Command palette (Ctrl+P) | createCommandPalette(world, config) |
bigText |
Large ASCII text | createBigText(world, config) |
canvas |
Drawing canvas | createCanvas(world, config) |
sparkline |
Inline chart | createSparkline(world, config) |
barChart |
Bar chart | createBarChart(world, config) |
lineChart |
Line chart | createLineChart(world, config) |
gauge |
Gauge/meter | createGauge(world, config) |
virtualizedList |
Performant large list | createVirtualizedList(world, config) |
accordion |
Collapsible sections | createAccordion(world, config) |
autocomplete |
Autocomplete input | createAutocomplete(world, config) |
calendar |
Date picker calendar | createCalendar(world, config) |
streamingText |
Streaming text display | createStreamingText(world, config) |
terminalWidget |
Embedded terminal | createTerminal(world, config) |
devTools |
ECS inspector | createDevTools(world, config) |
Systems (from blecsd/systems)
| System | Purpose | Phase |
|---|---|---|
inputSystem |
Process keyboard/mouse events | INPUT |
focusSystem |
Tab navigation, focus management | INPUT |
animationSystem |
Tweens, springs, easing | ANIMATION |
layoutSystem |
Position/size calculation | LAYOUT |
renderSystem |
Write entities to screen buffer | RENDER |
outputSystem |
Flush buffer to terminal | POST_RENDER |
collisionSystem |
AABB collision detection | UPDATE |
cameraSystem |
Camera viewport tracking | UPDATE |
particleSystem |
Particle emitter/updater | ANIMATION |
behaviorSystem |
AI/movement behaviors | UPDATE |
sceneGraphSystem |
Transform hierarchy | EARLY_UPDATE |
stateMachineSystem |
FSM transitions | UPDATE |
smoothScrollSystem |
Momentum scrolling | ANIMATION |
gameLoopSystem |
Fixed timestep physics | UPDATE |
workerPoolSystem |
Web worker management | UPDATE |
constraintLayout |
Constraint-based layout | LAYOUT |
frameBudgetSystem |
Frame time monitoring | POST_RENDER |
Common Patterns
Hello World
import {
createWorld, createScreenEntity, createBoxEntity,
setPosition, setDimensions, setText,
inputSystem, layoutSystem, renderSystem, outputSystem,
prepend,
} from 'blecsd';
const world = createWorld();
const screen = createScreenEntity(world, { width: 80, height: 24 });
const box = createBoxEntity(world, {
position: { x: 10, y: 5 },
dimensions: { width: 30, height: 5 },
border: { type: 'line' },
content: 'Hello, blECSd!',
});
prepend(world, screen, box);
// Run systems manually
inputSystem(world);
layoutSystem(world);
renderSystem(world);
outputSystem(world);
Using the Scheduler
import { createWorld, createScheduler, PhaseType } from 'blecsd';
const world = createWorld();
const scheduler = createScheduler();
scheduler.register(inputSystem, PhaseType.INPUT);
scheduler.register(layoutSystem, PhaseType.LAYOUT);
scheduler.register(renderSystem, PhaseType.RENDER);
scheduler.register(outputSystem, PhaseType.POST_RENDER);
// Game loop
function tick() {
scheduler.run(world);
requestAnimationFrame(tick);
}
tick();
Custom System
import { defineQuery, defineSystem, hasComponent } from 'blecsd';
import { Position } from 'blecsd/components';
import { Velocity } from 'blecsd/components';
const movingQuery = defineQuery([Position, Velocity]);
function createMovementSystem() {
return defineSystem((world) => {
const entities = movingQuery(world);
for (const eid of entities) {
Position.x[eid] += Velocity.x[eid];
Position.y[eid] += Velocity.y[eid];
}
return world;
});
}
Widget API Pattern
import { createBox, setBoxContent, isBox } from 'blecsd/widgets';
const box = createBox(world, {
position: { x: 0, y: 0 },
dimensions: { width: '100%', height: '100%' },
border: { type: 'line', fg: 0x00ff00 },
padding: { top: 1, left: 2 },
content: 'Initial content',
});
// Update content
setBoxContent(world, box, 'Updated content');
// Type check
if (isBox(world, box)) { /* ... */ }
Keyboard Shortcuts
Global: Tab (focus next), Shift+Tab (focus prev), Escape (blur). Lists: Up/k, Down/j, Home/g, End/G, PageUp/Down, Enter (select), / (search). Text input: Ctrl+A (start), Ctrl+E (end), Ctrl+U (delete to start), Ctrl+K (delete to end), Ctrl+W (delete word).
Error Handling
blECSd uses Result types for recoverable errors:
import { ok, err, isOk, isErr, map } from 'blecsd/errors';
function parseConfig(raw: unknown): Result<Config, ValidationError> {
const result = ConfigSchema.safeParse(raw);
if (!result.success) return err(createValidationError(result.error));
return ok(result.data);
}
Error categories: validation, terminal, system, entity, component, input, render, config, internal.
Testing
import { describe, it, expect } from 'vitest';
import { createWorld, addEntity, addComponent, hasComponent } from 'blecsd';
describe('movement system', () => {
it('updates position from velocity', () => {
const world = createWorld();
const eid = addEntity(world);
addComponent(world, eid, Position);
addComponent(world, eid, Velocity);
Position.x[eid] = 0;
Velocity.x[eid] = 5;
movementSystem(world);
expect(Position.x[eid]).toBe(5);
});
});
Common Anti-Patterns
- Using classes – All code must be functional. Use factory functions + plain objects.
- Importing from bitecs directly – Always import from
blecsdor../core/ecs. - Putting logic in component files – Components are data only. Logic goes in systems.
- Adding
worldto functions that don’t need it – Only ECS-aware functions need world. - Deep nesting – Use guard clauses and early returns.
- Using
any– Useunknownwith type guards. - Missing Zod validation at boundaries – All config objects need Zod schemas.
- Creating files when existing modules suffice – Search
src/before creating new files. - Forgetting barrel exports – Update the module’s
index.tswhen adding exports. - Processing input outside INPUT phase – All input goes through
inputSystem.
Module Ownership (Ambiguous Names)
Several function names exist in multiple modules:
| Function | Canonical Module | Notes |
|---|---|---|
moveCursor |
components/textInput/cursor |
6+ versions exist. Public API exports text-input version. |
fillRect |
terminal/screen/cell |
3D package has its own on PixelFramebuffer. |
getText |
components/content |
Rope utils also have getText but different signature. |
Always check which module you need. Use explicit subpath imports for non-canonical versions.
Development Commands
pnpm install # Install dependencies
pnpm dev # Development mode
pnpm build # Build (run frequently, catches issues tests miss)
pnpm test # Run tests
pnpm test:watch # Watch mode
pnpm lint # Biome linter
pnpm lint:fix # Auto-fix lint
pnpm typecheck # TypeScript type check
Performance Tips
- Cache queries:
const myQuery = defineQuery([Position, Velocity])once, reuse everywhere. - Batch component reads: access all components for an entity in one pass.
- Use dirty tracking: only re-render entities that changed.
- Virtualize large lists with
createVirtualizedList. - Use double buffering (
createDoubleBuffer) for flicker-free rendering. - Avoid allocations in hot loops: reuse objects, pre-allocate arrays.
- Use
frameBudgetSystemto monitor frame times.
Add-on Packages
blECSd has optional packages for specialized functionality:
| Package | Import | Purpose |
|---|---|---|
@blecsd/3d |
import { ... } from '@blecsd/3d' |
3D rendering with software rasterizer |
@blecsd/ai |
import { ... } from '@blecsd/ai' |
AI/LLM interface widgets |
@blecsd/game |
import { ... } from '@blecsd/game' |
High-level game API |
@blecsd/audio |
import { ... } from '@blecsd/audio' |
Audio management |
@blecsd/media |
import { ... } from '@blecsd/media' |
Image/video/GIF rendering |