go-tui Framework

repository·main·Indexed 18 days ago

https://github.com/grindlemire/go-tui

A framework for building reactive terminal user interfaces in Go using GSX, a declarative, HTML-like syntax. It features a flexbox layout engine, reactive state management via State[T], and a compiler that generates type-safe Go code. The ecosystem includes a tui LSP for real-time diagnostics and auto-completion, with official plugin support for Neovim and VS Code.

Tokens
167.9K
Snippets
565
Records
722
Agent score
60%

What's inside go-tui

  1. Overview of the GSX Language Server

    main

    The GSX Language Server is an LSP (Language Server Protocol) implementation specifically for .gsx files. It provides real-time editor intelligence features including:

    • Hover: Documentation on hover.
    • Completion: Code completion suggestions.
    • Go-to-definition: Navigating to where an element is defined.
    • Find-references: Locating all usages of an element.
    • Diagnostics: Real-time error and warning reporting.
    • Formatting: Document auto-formatting.
    • Semantic tokens: Semantic token highlighting.
    • Symbols: Document and workspace-wide symbol discovery.
  2. Project directory structure overview

    main

    Understanding the project layout helps in locating core logic and generated files:

    • cmd/tui/: Contains the tui CLI tool source.
    • pkg/tui/: The core TUI engine (app loop, buffer, rendering, widgets).
    • pkg/layout/: The layout engine (calculation, nodes, styles).
    • pkg/tuigen/: The code generation logic (lexer, parser, generator).
    • examples/: Sample applications (hello, dashboard, form).
  3. Overview of GSX Syntax and Workflow

    main

    .gsx files are Go source files extended with a template syntax for declaring UIs. The tui generate command processes these files to produce standard Go code in _gsx.go files.

    Important: Never edit the generated _gsx.go files manually; always modify the .gsx source.

    A .gsx file can contain:

    • Standard Go package declarations and import blocks.
    • Go type declarations and functions (type, func).
    • Pure template components (templ Name(params) { ... }).
    • Struct method components (templ (s *Struct) Render() { ... }).
    • Control flow directives (if, for, :=).
    • HTML-like elements (e.g., <div>, <span>).
    package mypackage
    
    import (
        "fmt"
        tui "github.com/grindlemire/go-tui"
    )
    
    // You can mix Go declarations and templ components in any order
    type MyData struct {
        Value string
    }
    
    templ MyComponent(d MyData) {
        <span>{d.Value}</span>
    }
  4. Overview of GSX Syntax

    main

    .gsx files are Go files extended with a templ-like syntax for declaring UI. They support standard Go package declarations, imports, and type definitions. The templ keyword is used to define components that return element trees.

    To use GSX, run the tui generate command, which reads .gsx files and produces standard _gsx.go files that call the tui package API. Note: You should never edit the generated _gsx.go files directly.

  5. Overview of go-tui built-in components

    main

    go-tui provides three core built-in components for building terminal user interfaces:

    1. Input: A single-line text input component with cursor management, horizontal scrolling, and placeholder support.
    2. TextArea: A multi-line text input component.
    3. Modal: An overlay dialog component that handles backdrop rendering, focus trapping, and preemptive key blocking.

    All three components implement the Component, KeyListener, and AppBinder interfaces.

    Important Usage Note: When using these as GSX elements (<input>, <textarea>, <modal>), they must be mounted against a component's receiver. Therefore, place them inside a struct method component rather than a pure templ function. The same rule applies to the <markdown> element.

  6. What is a Ref and how to use it

    main

    A Ref is a pointer to a single element in the rendered tree, allowing you to interact with it from Go code. You create a ref using tui.NewRef() and attach it to an element in your GSX template using the ref attribute.

    Important: Before the first render, ref.El() returns nil. Always perform a nil check before accessing the element.

    type myApp struct {
        saveBtn *tui.Ref
    }
    
    func MyApp() *myApp {
        return &myApp{
            saveBtn: tui.NewRef(),
        }
    }
    templ (a *myApp) Render() {
        <button ref={a.saveBtn} class="px-2">Save</button>
    }
    // Accessing the element safely
    if el := a.saveBtn.El(); el != nil {
        // safe to use el
    }
    if el := a.saveBtn.El(); el != nil {
        // safe to use el
    }
  7. StreamAbove vs PrintAbove

    main

    Choose between PrintAbove and StreamAbove based on your output requirements:

    FeaturePrintAboveStreamAbove
    InputComplete formatted stringIncremental text via *StreamWriter
    StylingManual ANSI in format stringWriteStyled, WriteGradient, or raw ANSI
    Thread SafetyUse QueuePrintAbove from goroutinesWriter is goroutine-safe by default
    Best ForChat messages, log lines, status updatesLLM token streaming, progressive output
    Element InsertionUse PrintAboveElementUse WriteElement on the writer
  8. How the go-tui layout engine works

    main

    go-tui uses a CSS flexbox-compatible layout engine. Every <div> acts as a flex container, and its children are flex items. You can arrange children using a main axis (the direction children flow) and a cross axis (perpendicular to the main axis).

    Layout controls can be applied in two ways:

    1. Tailwind-style classes: Using strings like flex-col, justify-center, or gap-2 in the class attribute.
    2. Element attributes: Using typed attributes like direction={tui.Column}, justify={tui.JustifyCenter}, or gap={2}.
  9. Manage focus with FocusGroups

    main

    Focus can be managed at the element level (element.Focus(), element.Blur()) or the app level (app.FocusNext(), app.FocusPrev()).

    For section-level switching (e.g., between a sidebar and content), use a FocusGroup. A FocusGroup created with tui.MustNewFocusGroup(compA, compB) allows cycling focus between its members using fg.Next() or fg.Prev(). The group's KeyMap() automatically provides Tab and Shift+Tab bindings.

    // FocusGroup example
    sidebar := tui.NewState(true)
    content := tui.NewState(false)
    fg := tui.MustNewFocusGroup(sidebar, content)
    fg.Next() // Cycles focus
  10. Configure mouse behavior for Markdown rendering

    main

    When rendering Markdown, you can choose between native terminal interaction or captured mouse events using tui.WithoutMouse() or tui.WithMouse().

    This mode leaves the terminal's native behavior intact. This allows users to:

    • Select and copy text normally.
    • Click OSC 8 hyperlinks (on terminals like Ghostty, iTerm2, kitty, or WezTerm).
    • Use the mouse wheel to scroll (via alternate-scroll DEC mode 1007, which translates wheel notches to arrow keys).

    To ensure copied text is clean, the viewer draws no borders or padding, and uses erase-to-end-of-line instead of writing spaces to prevent trailing whitespace in selections.

    Using tui.WithMouse()

    This mode captures mouse events for click and wheel events. Note that native terminal selection and link opening will be disabled unless the user holds the terminal's bypass modifier (usually Shift).

  11. How component lifecycles and interfaces work in go-tui

    main

    go-tui uses interfaces to define component behavior. A struct component only needs to implement the Component interface (the Render method). All other interfaces are optional and are discovered via type assertions during the mount process.

    Initial Mount Order

    When a component first mounts, the framework executes operations in this specific order:

    1. AppBinder.BindApp: Wires up State and Events fields.
    2. Initializer.Init: Runs setup logic and captures a cleanup function.
    3. Component.Render: Produces the element tree.
    4. WatcherProvider.Watchers: Discovers and starts background watchers.
    5. KeyListener.KeyMap and MouseListener.HandleMouse: Discovered during event dispatch via tree walks.

    Subsequent Renders (Cached Mount)

    On re-renders of a cached component, the framework:

    1. Calls PropsUpdater.UpdateProps (if implemented).
    2. Re-calls AppBinder.BindApp.
    3. Re-calls Render.
  12. Create key bindings with KeyMatcher

    main

    A KeyMatcher defines which key events a binding should respond to. You can use three types of matchers:

    • Key constants: Use tui.KeyEscape, tui.KeyEnter, etc., to match specific special keys.
    • tui.Rune(r rune): Matches a specific printable character.
    • tui.AnyRune: Matches any printable character.

    All matchers support modifier methods to require specific combinations:

    • .Shift()
    • .Ctrl()
    • .Alt()
    tui.KeyUp.Shift()       // Match Shift+Up
    tui.KeyUp.Ctrl()        // Match Ctrl+Up
    tui.KeyUp.Alt()         // Match Alt+Up
    tui.Rune('s').Ctrl()    // Match Ctrl+S
    tui.Rune('x').Alt()     // Match Alt+X