Storm Terminal UI Framework

repository·main·Indexed 18 days ago

https://github.com/orchetron/storm

A 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.

Tokens
97.3K
Snippets
289
Records
356
Agent score
63%

What's inside @orchetron/storm

  1. Use Content Components for display

    main
    Storm provides a suite of Content Components designed for structured data display. These components include layout elements like Card, typography elements like Heading and Paragraph, media elements like Image, and decorative utility elements like Gradient, GradientBorder, GlowText, Shadow, and RichLog.
  2. Customize component styles using tiers

    main

    Storm uses a three-tiered styling system. Depending on the component, you can apply styles at different levels of complexity:

    1. Tier 1 -- Text styles: For inline components like Text, Badge, or Spinner. Supports props like color, bold, and dim.
    2. Tier 2 -- Layout styles: Adds sizing and margin. Supports props like width and margin.
    3. Tier 3 -- Container styles: For components like Card. Adds padding, borderStyle, borderColor, and backgroundColor.

    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>
  3. Understand the Storm Render Pipeline

    main

    Storm uses a multi-stage pipeline to transform React element trees into minimal ANSI terminal updates. The process follows these steps:

    1. React commit: The reconciler (reconciler/host.ts) mutates the element tree.
    2. FrameScheduler: Throttles updates to a maximum FPS, coalesces rapid commits, and detects render loops.
    3. RenderPipeline.fullPaint(): Orchestrates the rendering process:
      • Runs beforeRender plugins.
      • Runs runLayout middleware.
      • Executes paint(): Runs computeLayout() and writes cells into a ScreenBuffer.
      • Runs runPaint middleware (post-processing).
      • Calls Screen.flush() to hand the buffer to the DiffRenderer.
      • DiffRenderer.render(): Diffs the previous and next buffers to emit minimal ANSI sequences.
    4. Incremental repaint: Triggered by requestRender(). This bypasses the React commit phase and is used for high-performance updates like animations or scrolling.
  4. Correctly nest Box and Text components

    main

    Text is intended for styled inline content, while Box is for layout.

    Do not nest a Box inside a Text component, as this breaks layout calculations because the reconciler treats Text children as inline content rather than layout nodes. Instead, use a Box as the parent and place Text components inside it.

    Allowed: Nesting Text inside Text for 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>
  5. Bridge .storm.css variables to ThemeProvider

    main

    You can pass variables from a .storm.css file into the ThemeProvider using the --storm-{group}-{key} naming convention. The useStyleSheet hook extracts these as themeOverrides.

    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.

  6. Define a Locale object

    main

    A Locale object defines the linguistic and formatting rules for a language. It includes the ISO 639-1 code, text direction (ltr or rtl), number formatting rules, month and weekday names, and a dictionary of translatable strings. You can also optionally provide a pluralRule.

    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",
      },
    };
  7. Understand Storm widget architecture and animation

    main

    When building with Storm widgets, keep these architectural patterns in mind:

    • Imperative Animation Pattern: Animated widgets (like BlinkDot, ShimmerText, StreamingText, and OperationTree) use ref mutation and requestRender() 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 the usePersonality() 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.
  8. Choose an animation approach in Storm TUI

    main

    Storm TUI provides two primary animation paradigms depending on your use case:

    1. 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 global AnimationScheduler.
    2. 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 useAnimation for frame-based patterns (e.g., cycling through text frames).
    • Use useTransition for 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.
  9. How the Personality system works

    main

    While 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} />;
    }
  10. Intercept and modify input events

    main

    The onKey and onMouse hooks 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 event object.
    • 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;
      },
    };
  11. Override component props and set defaults

    main

    There are two ways to manage component properties via plugins:

    1. 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.
    2. 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, or undefined to pass through.

    Processing Order:

    1. Merge componentDefaults from all plugins.
    2. Apply user-provided props (user props always win).
    3. Run onComponentProps from 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;
      },
    };