Storm Terminal UI Framework
repository·main·Indexed 18 days ago
https://github.com/orchetron/stormA compositor-based terminal UI framework for high-performance, layered interfaces. Storm treats the terminal as a display server, enabling 60fps animations and complex layouts using React and CSS-like styling. It includes a comprehensive library of components for data display, input, visualization, and specialized AI agent widgets like MessageBubble and OperationTree, along with built-in DevTools for render heatmaps and accessibility audits.
What's inside @orchetron/storm
- Storm provides a set of fundamental building blocks designed for layout and text rendering. These core components serve as the primary primitives for constructing user interfaces within the Storm ecosystem.
Use Content Components for display
mainStorm provides a suite of Content Components designed for structured data display. These components include layout elements likeCard, typography elements likeHeadingandParagraph, media elements likeImage, and decorative utility elements likeGradient,GradientBorder,GlowText,Shadow, andRichLog.Customize component styles using tiers
mainStorm uses a three-tiered styling system. Depending on the component, you can apply styles at different levels of complexity:
- Tier 1 -- Text styles: For inline components like
Text,Badge, orSpinner. Supports props likecolor,bold, anddim. - Tier 2 -- Layout styles: Adds sizing and margin. Supports props like
widthandmargin. - Tier 3 -- Container styles: For components like
Card. Addspadding,borderStyle,borderColor, andbackgroundColor.
To override a component's default look, pass the corresponding prop directly to the component.
// Tier 1 <Text color="#82AAFF" bold dim>Styled text</Text> // Tier 2 <Button label="Click" width={20} margin={1} color="#34D399" /> // Tier 3 <Card borderStyle="round" borderColor="#82AAFF" padding={2} backgroundColor="#141414" > <Text>Content</Text> </Card>- Tier 1 -- Text styles: For inline components like
Understand the Storm Render Pipeline
mainStorm uses a multi-stage pipeline to transform React element trees into minimal ANSI terminal updates. The process follows these steps:
- React commit: The reconciler (
reconciler/host.ts) mutates the element tree. - FrameScheduler: Throttles updates to a maximum FPS, coalesces rapid commits, and detects render loops.
- RenderPipeline.fullPaint(): Orchestrates the rendering process:
- Runs
beforeRenderplugins. - Runs
runLayoutmiddleware. - Executes
paint(): RunscomputeLayout()and writes cells into aScreenBuffer. - Runs
runPaintmiddleware (post-processing). - Calls
Screen.flush()to hand the buffer to theDiffRenderer. DiffRenderer.render(): Diffs the previous and next buffers to emit minimal ANSI sequences.
- Runs
- Incremental repaint: Triggered by
requestRender(). This bypasses the React commit phase and is used for high-performance updates like animations or scrolling.
- React commit: The reconciler (
Correctly nest Box and Text components
mainTextis intended for styled inline content, whileBoxis for layout.Do not nest a
Boxinside aTextcomponent, as this breaks layout calculations because the reconciler treatsTextchildren as inline content rather than layout nodes. Instead, use aBoxas the parent and placeTextcomponents inside it.Allowed: Nesting
TextinsideTextfor inline styling is supported.// RIGHT: Box handles layout, Text handles styling <Box> <Text color="green">Status: </Text> <Box width={10}><Text>OK</Text></Box> </Box> // ALLOWED: Inline style nesting <Text> Hello <Text bold>world</Text>, welcome to <Text color="cyan">Storm</Text> </Text>Bridge .storm.css variables to ThemeProvider
mainYou can pass variables from a
.storm.cssfile into theThemeProviderusing the--storm-{group}-{key}naming convention. TheuseStyleSheethook extracts these asthemeOverrides.Naming Convention
- Flat fields:
--storm-success,--storm-warning,--storm-error,--storm-info,--storm-divider - Nested fields:
--storm-brand-primary,--storm-text-dim,--storm-surface-base(hyphens after the group name are converted to camelCase).
Valid Group Names
brand,text,surface,system,user,assistant,thinking,tool,approval,input,diff,syntax.- Flat fields:
Define a Locale object
mainA
Localeobject defines the linguistic and formatting rules for a language. It includes the ISO 639-1 code, text direction (ltrorrtl), number formatting rules, month and weekday names, and a dictionary of translatable strings. You can also optionally provide apluralRule.import { EN, PLURAL_FR, type Locale } from "@orchetron/storm"; const FR: Locale = { code: "fr", direction: "ltr", pluralRule: PLURAL_FR, numbers: { decimal: ",", thousands: " ", grouping: 3 }, months: [ "janvier", "fevrier", "mars", "avril", "mai", "juin", "juillet", "aout", "septembre", "octobre", "novembre", "decembre", ], monthsShort: [ "janv.", "fevr.", "mars", "avr.", "mai", "juin", "juil.", "aout", "sept.", "oct.", "nov.", "dec.", ], weekdays: [ "dimanche", "lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi", ], weekdaysShort: ["dim.", "lun.", "mar.", "mer.", "jeu.", "ven.", "sam."], strings: { "greeting": "Bonjour, {name} !", "items.one": "{count} element", "items.other": "{count} elements", }, };Understand Storm widget architecture and animation
mainWhen building with Storm widgets, keep these architectural patterns in mind:
- Imperative Animation Pattern: Animated widgets (like
BlinkDot,ShimmerText,StreamingText, andOperationTree) use ref mutation andrequestRender()instead of React state. This is necessary because Storm's custom React reconciler does not flush state updates like React DOM. - Plugin System: Every widget wraps its props through
usePluginProps("WidgetName", rawProps), allowing plugins to intercept and modify widget properties globally. - Personality System: Animated widgets derive timing defaults (e.g.,
durationFast,durationSlow) from theusePersonality()hook. - Automatic Cleanup: All timer-based widgets use
useCleanup()to prevent memory leaks or interval errors when components unmount. - Performance: All widgets are wrapped in
React.memo()to minimize re-renders within the Storm reconciler.
- Imperative Animation Pattern: Animated widgets (like
Choose an animation approach in Storm TUI
mainStorm TUI provides two primary animation paradigms depending on your use case:
- Imperative (
useAnimation): Best for continuous, frame-based animations like spinners or progress bars. It provides direct control over frame updates and avoids React reconciliation overhead by using a globalAnimationScheduler. - Declarative (
useTransition,<Transition>,<AnimatePresence>): Best for UI state changes, such as showing/hiding elements, enter/exit patterns, and animating numeric values. These are simpler to use for common UI transitions.
Decision Guide:
- Use
useAnimationfor frame-based patterns (e.g., cycling through text frames). - Use
useTransitionfor numeric interpolation (e.g., animating opacity or position) with fine control (delay, spring easing). - Use
<Transition>for simple show/hide patterns. - Use
<AnimatePresence>for dynamic lists where items need to play an exit animation before unmounting.
- Imperative (
How the Personality system works
mainWhile a Theme defines the color palette, a Personality defines the complete interaction identity. It includes colors, but also specifies borders, animation timings, typography, and interaction characters (like prompt or selection characters).
Use the
usePersonality()hook to access these properties. This allows components to adapt their behavior (e.g., animation speed or spinner type) based on the active personality.import { usePersonality } from "@orchetron/storm"; function MyComponent() { const personality = usePersonality(); const spinnerType = personality.animation.spinnerType; // e.g., "diamond" const promptChar = personality.interaction.promptChar; // e.g., "›" const selectionChar = personality.interaction.selectionChar; // e.g., "◆" return <Spinner type={spinnerType} />; }Intercept and modify input events
mainThe
onKeyandonMousehooks act as a middleware chain. Plugins receive events in registration order.- To consume an event: Return
null. The event is dropped and will not reach components. - To pass an event through: Return the original
eventobject. - To modify an event: Return a new event object with the desired changes (e.g., remapping keys).
const loggingPlugin: StormPlugin = { name: "input-logger", onKey(event) { // Log every keypress but don't consume it console.log(`Key: ${event.key}, ctrl=${event.ctrl}`); return event; }, onMouse(event) { // Block all mouse clicks in a specific region if (event.x < 10 && event.y < 5) { return null; // consumed -- components won't see it } return event; }, };- To consume an event: Return
Override component props and set defaults
mainThere are two ways to manage component properties via plugins:
componentDefaults(Declarative): A record of default props for specific components. These are applied BEFORE user-provided props. If multiple plugins define defaults, they are merged in registration order.onComponentProps(Imperative): A callback that runs for every component render. It receives the component name and current props (after defaults are applied). Return the modified props object to apply changes, orundefinedto pass through.
Processing Order:
- Merge
componentDefaultsfrom all plugins. - Apply user-provided props (user props always win).
- Run
onComponentPropsfrom each plugin in registration order.
// Example of componentDefaults const compactPlugin: StormPlugin = { name: "compact-layout", componentDefaults: { Box: { paddingX: 0, paddingY: 0 }, Text: { wrap: "truncate" }, Select: { maxVisible: 5 }, }, }; // Example of onComponentProps const highContrastPlugin: StormPlugin = { name: "high-contrast", onComponentProps(componentName, props) { if (componentName === "Text") { return { ...props, bold: true }; } return undefined; }, };