Gridland Framework

repository·main·Indexed 16 days ago

https://github.com/thoughtfulllc/gridland

A framework for building terminal applications (TUIs) using React that run in both the browser and the terminal, powered by the OpenTUI rendering engine. It includes @gridland/bun for terminal execution and binary compilation, @gridland/web for HTML5 canvas rendering, @gridland/container for isolated Docker sandboxing, and a scaffolding tool via create-gridland.

Tokens
93.4K
Snippets
322
Records
421
Agent score
60%

What's inside Gridland

  1. What is Gridland?

    main

    Gridland is a UI framework that allows you to render the same React component tree to multiple environments: a browser canvas, a terminal (TUI), or plain text for AI agents.

    Key characteristics:

    • Dual Runtimes: Use @gridland/web to draw components to an HTML5 <canvas> in a browser, or @gridland/bun to draw them to stdout in a terminal via a native FFI bridge.
    • Agent-Friendly: Components can render to plain text, making TUIs natively readable by LLMs, crawlers, and screen readers without a separate accessibility layer.
    • Component Ownership: Gridland uses a distribution model similar to shadcn/ui. Instead of installing a monolithic package, you add component source code directly to your project using the CLI, allowing for full customization.
    • Cell-Based Layout: Layouts are sized in character cells rather than pixels, ensuring perfect alignment across different terminal widths and font sizes. It uses the Yoga engine for Flexbox layout.
  2. Run apps in an isolated sandbox with @gridland/container

    main

    The @gridland/container package allows you to run Gridland apps or any CLI tool inside an isolated Docker container. This provides a safe sandbox for executing untrusted terminal code, making it ideal for agent tools, code playgrounds, and review environments where you want to prevent code from accessing your host machine.

    Requirements

    • Docker must be installed and running.
    • Bun version 1.0 or later.
    # Run a demo in a sandbox
    bunx @gridland/container @gridland/demo -- landing
  3. How Gridland rendering targets work

    main

    Gridland is JSX that renders to a cell grid rather than HTML. This allows the same component tree to be targeted at different environments:

    1. In a browser: Uses @gridland/web to draw to an HTML5 <canvas>.
    2. In a terminal: Uses @gridland/bun to draw to stdout via a native FFI bridge.
    3. As plain text (Headless): Renders the component tree to a plain-text string. This is a first-class target designed for AI agents, crawlers, and screen readers to ensure accessibility and searchability without needing a canvas or terminal.

    Because all three paths accept the same components and hooks, you can share a single codebase to ship a web TUI, a CLI binary, and an agent-readable document simultaneously.

  4. Manage PromptInput state with PromptInputProvider

    main

    Wrap your application in PromptInputProvider to lift the input state (text, suggestions, etc.) outside of the PromptInput component. This allows sibling components to interact with the input.

    Use usePromptInputController to access and modify the state:

    • controller.textInput.value: current text
    • controller.textInput.setValue(v): set text
    • controller.textInput.clear(): clear text
    • controller.suggestions.setSuggestions(s): update suggestions
    import { PromptInputProvider, usePromptInputController } from "@/components/ui/prompt-input"
    
    <PromptInputProvider initialInput="">
      <Sidebar />
      <PromptInput focusId="prompt" autoFocus onSubmit={handleSubmit} />
    </PromptInputProvider>
    import { PromptInputProvider, usePromptInputController } from "@/components/ui/prompt-input"
    
    <PromptInputProvider initialInput="">
      <Sidebar />
      <PromptInput focusId="prompt" autoFocus onSubmit={handleSubmit} />
    </PromptInputProvider>
  5. Security model of @gridland/container

    main

    Every container managed by @gridland/container runs with hardened security defaults to ensure isolation:

    • Capabilities: All Linux capabilities are dropped (--cap-drop=ALL).
    • Privileges: Prevents privilege escalation (--security-opt=no-new-privileges).
    • Filesystem: The root filesystem is read-only (--read-only).
    • Process Limits: Limits the process count to 256 (--pids-limit=256).
    • Memory: Defaults to a 512MB cap (--memory=512m).
    • User: Runs as an unprivileged runner user (non-root).
    • Scratch Space: A writable tmpfs is provided at /tmp, limited to 256MB.

    Warning: Network access is enabled by default. Always use the --no-network flag when running untrusted code that should not make outbound connections.

  6. How Tabs and TabsList work together

    main

    The Tabs component is a compound component system used to manage tabbed interfaces. It consists of several parts:

    • Tabs: The root container that manages the active tab state.
    • TabsList: The horizontal bar containing the triggers. It is focused-is-interactive, meaning that when the focusId is focused, arrow/h/l keys trigger navigation immediately without an extra "Enter" step.
    • TabsTrigger: Defines a specific tab option. It must have a value that matches a TabsContent component.
    • TabsContent: The panel that renders its children only when its value matches the active tab.

    Note: For keyboard navigation to work, your application must be wrapped in a GridlandProvider or a FocusProvider so the focus system can route keys.

    Keyboard Controls:

    • / h: Previous tab (wraps around)
    • / l: Next tab (wraps around)
    import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tab-bar"
    
    <Tabs defaultValue="files">
      <TabsList focusId="tabs" autoFocus>
        <TabsTrigger value="files">Files</TabsTrigger>
        <TabsTrigger value="search">Search</TabsTrigger>
        <TabsTrigger value="git">Git</TabsTrigger>
      </TabsList>
      <TabsContent value="files">
        <text>Browse project files</text>
      </TabsContent>
      <TabsContent value="search">
        <text>Search across files</text>
      </TabsContent>
      <TabsContent value="git">
        <text>View git status</text>
      </TabsContent>
    </Tabs>
  7. Handle Spinner completion states

    main

    The status prop allows you to transition the spinner from an animated state to a static completion symbol. When a status is provided, the animation stops and a semantic icon is displayed using the appropriate theme color.

    StatusSymbolColor
    successtheme.success
    errortheme.error
    warningtheme.warning
    infotheme.accent

    Note: The default status is spinning.

    <Spinner status="success" text="Dependencies installed" />
    <Spinner status="error" text="Build failed" />
    <Spinner status="warning" text="Compiled with warnings" />
    <Spinner status="info" text="3 tasks queued" />
  8. How BrowserContext works with TUI

    main

    When you mount the <TUI> component, it installs a BrowserContext for all its descendants. This context acts as the single source of truth for the renderer, the canvas element, and cell dimensions.

    Use the useBrowserContext() hook inside any component that is a descendant of <TUI> to access the current BrowserContextValue. This allows browser-only hooks like useFileDrop and usePaste to function correctly.

    import { BrowserContext, useBrowserContext } from "@gridland/web"
    
    // Inside a component descendant of <TUI>
    const context = useBrowserContext();
  9. Register slash commands via CommandProvider

    main

    Instead of passing a commands array directly to PromptInput, you can use a CommandProvider to allow any component in the tree to register slash commands. This is useful for modular architectures where different parts of the UI provide different commands.

    Hooks for Command Registration:

    • useRegisterCommand({ cmd, desc, onExecute }): Registers a single command. Automatically unregisters on unmount.
    • useRegisterCommands([{ cmd, desc }, ...]): Registers multiple commands at once.
    • useRegistryCommands(): Returns a reactive list of all currently registered PromptInputCommand objects.
    import { CommandProvider, useRegisterCommand } from "@/components/ui/prompt-input"
    
    function ModelSwitcher() {
      useRegisterCommand({ cmd: "/model", desc: "Switch model" })
      return null
    }
    
    <CommandProvider>
      <ModelSwitcher />
      <PromptInput focusId="prompt" autoFocus onSubmit={handleSubmit} />
    </CommandProvider>
    import { CommandProvider, useRegisterCommand } from "@/components/ui/prompt-input"
    
    function ModelSwitcher() {
      useRegisterCommand({ cmd: "/model", desc: "Switch model" })
      return null
    }
    
    function ClearButton() {
      useRegisterCommand({ cmd: "/clear", desc: "Clear conversation", onExecute: () => clearChat() })
      return null
    }
    
    <CommandProvider>
      <ModelSwitcher />
      <ClearButton />
      <PromptInput focusId="prompt" autoFocus onSubmit={handleSubmit} />
    </CommandProvider>
  10. Configure Global Keyboard Handlers with useKeyboard

    main

    The useKeyboard hook is used to register keyboard event handlers. It supports two scoping modes:

    1. Global Handlers (Recommended): To register a handler that works regardless of which element has focus, pass { global: true } in the options object.
    2. Scoped Handlers (Deprecated): The bare form useKeyboard(handler) is deprecated. It is intended for handlers that should only fire when a specific component's subtree is mounted or focused.

    Note: When migrating, be careful not to convert scoped handlers (like those in Modals or Tab Bars) to global handlers, as this will cause them to intercept keys even when they are not the active focus target.

    // Global handler (Blessed form)
    useKeyboard(handler, { global: true });
    
    // Scoped handler (Deprecated bare form)
    useKeyboard(handler);