NIMWAVE Documentation

repository·master·Indexed 19 days ago

https://github.com/ansiwave/nimwave

A library for building Text User Interfaces (TUIs) decoupled from the terminal, enabling the same UI code to run on the terminal, desktop (via OpenGL/GLFW), or web (via WebAssembly). It utilizes a tree-like hierarchy of nodes, custom node implementation via nw.Node, and state management through mounted nodes and a customizable Context.

Tokens
1.7K
Snippets
6
Records
7
Agent score
19%

What's inside NIMWAVE

  1. Build UIs with a hierarchy of nodes

    master

    NIMWAVE uses a tree-like hierarchy of nodes to construct user interfaces. You render the UI by calling the render function with a root node and a Context. Built-in nodes like nw.Box allow you to define layout directions and borders.

    render(
      nw.Box(
        direction: nw.Direction.Vertical,
        border: nw.Border.Single,
        children: nw.seq(
          "Hello, world!",
          "Nim rocks",
        ),
      ),
      ctx
    )
  2. Manage local state with Stateful Nodes

    master

    To maintain state across render calls (e.g., a counter or a button's toggle state), a node must be "mounted".

    1. Mounting: Use getMounted(node, ctx) to retrieve the persistent version of the node. If it hasn't been mounted, NIMWAVE stores it in the context.
    2. State Access: Use the object returned by getMounted (often called mnode) to read/write persistent state. Use the original node argument to read transient data like mouse input.
    3. Lifecycle: You can define mount and unmount methods (matching the render signature) to run custom setup/teardown code.
    4. Uniqueness: Every mounted node must have a unique id. It is recommended to use hierarchical IDs (e.g., node.id & "/child_id").
    type
      Counter = ref object of nw.Node
        mouse: iw.MouseInfo
        count: int
    
    method render*(node: Counter, ctx: var nw.Context[State]) =
      let mnode = getMounted(node, ctx) # mnode holds the persistent state
      ctx = nw.slice(ctx, 0, 0, 15, 3)
      
      proc incCount() = 
        mnode.count += 1
    
      render(
        nw.Box(
          direction: nw.Direction.Horizontal,
          border: nw.Border.None,
          children: nw.seq(
            nw.Box(
              direction: nw.Direction.Horizontal,
              border: nw.Border.Hidden,
              children: nw.seq($mnode.count),
            ),
            Button(str: "Count", mouse: node.mouse, action: incCount),
          ),
        ),
        ctx
      )
    
    # Rendering the stateful node
    render(Counter(id: "counter", mouse: mouse), ctx)
  3. Store global state in the Context

    master

    For state that needs to be shared across many nodes (like a focus system or global settings), define a custom type for the Context data. This data is accessible via ctx.data.

    Important: When defining your state object, use ref types for data that must be shared and mutated by multiple nodes. Value types will be copied, meaning modifications inside a node will only affect that node's children.

    type
      State = object
        focusIndex*: int
        focusAreas*: ref seq[iw.TerminalBuffer]
    
    var ctx = nw.initContext[State]()
    
    # Accessing global state in a node
    let focused = addFocusArea(ctx)
  4. Create custom nodes

    master

    You can define custom nodes by creating a type that inherits from nw.Node (typically using ref object of nw.Node) and implementing the render* method. The render method defines how your node's data is transformed into built-in nodes or low-level drawing operations.

    type
      MyCustomNode = ref object of nw.Node
        lines: seq[string]
    
    method render*(node: MyCustomNode, ctx: var nw.Context[State]) =
      render(
        nw.Box(
          direction: nw.Direction.Vertical,
          border: nw.Border.Single,
          children: nw.seq(node.lines),
        ),
        ctx
      )
    
    # Usage:
    render(MyCustomNode(lines: @["Hello, world!", "Nim rocks"]), ctx)
  5. Apply styling and colors via illwave

    master

    Low-level rendering is handled via illwave. You can manipulate individual cells in the TerminalBuffer (ctx.tb) using relative coordinates (where 0, 0 is the top-left of the current node). You can change foreground (fg), background (bg), and characters (ch). Alternatively, you can use ANSI escape codes within nw.Text nodes.

    # Direct cell manipulation
    var cell = ctx.tb[0, 0]
    cell.fg = iw.fgBlue
    cell.bg = iw.bgYellow
    cell.ch = "Z".toRunes[0]
    ctx.tb[0, 0] = cell
    
    # Using ANSI escape codes in Text nodes
    render(nw.Text(str: "\e[32;43mHello, world!\e[0m"), ctx)
  6. Resize nodes using nw.slice

    master

    By default, a node inherits its size from its parent. You can retrieve the parent-provided dimensions using iw.width(ctx.tb) and iw.height(ctx.tb). To change a node's dimensions, use nw.slice to redefine the context's bounding box before rendering children.

    method render*(node: MyCustomNode, ctx: var nw.Context[State]) =
      # Resizes the context to the parent's width but a specific height
      ctx = nw.slice(ctx, 0, 0, iw.width(ctx.tb), node.lines.len+2)
      render(
        nw.Box(
          direction: nw.Direction.Vertical,
          border: nw.Border.Single,
          children: nw.seq(node.lines),
        ),
        ctx
      )