Using kvx as a Library#

This guide covers everything you need to embed kvx in your own Go application — from a five-line quickstart to advanced customization.

Install#

go get github.com/oakwood-commons/kvx@latest

Quickstart#

There are two public packages. Pick the one that fits your use case:

PackageImportUse when you want…
pkg/coregithub.com/oakwood-commons/kvx/pkg/coreLoad data, evaluate expressions, render plain text tables — no terminal UI
pkg/tuigithub.com/oakwood-commons/kvx/pkg/tuiLaunch the full interactive TUI or render bordered tables

Minimal example — render a table (no TUI)#

package main

import (
    "fmt"

    "github.com/oakwood-commons/kvx/pkg/core"
    "github.com/oakwood-commons/kvx/pkg/tui"
)

func main() {
    data := map[string]any{
        "name":    "my-service",
        "version": "2.1.0",
        "healthy": true,
    }

    root, _ := core.LoadObject(data)

    // Bordered table, just like the kvx CLI
    fmt.Print(tui.RenderTable(root, tui.TableOptions{
        AppName:  "my-app",
        Path:     "_",
        Bordered: true,
    }))
}

Minimal example — launch the interactive TUI#

root, _ := core.LoadFile("data.yaml")

cfg := tui.DefaultConfig()
cfg.AppName = "my-app"

if err := tui.Run(root, cfg); err != nil {
    log.Fatal(err)
}

That’s it. Everything else below is optional.


Loading Data#

All loaders return a generic interface{} (map, slice, or scalar) that every other kvx function accepts. Format is auto-detected.

// From a file (JSON, YAML, NDJSON, JWT — auto-detected)
root, err := core.LoadFile("config.yaml")

// From a string
root, err := core.LoadRoot(`{"key": "value"}`)

// From bytes (useful for HTTP responses, embedded data, etc.)
root, err := core.LoadRootBytes(bodyBytes)

// From an existing Go value — map, slice, or struct
root, err := core.LoadObject(myStruct)

Custom structs#

LoadObject automatically converts structs to maps via JSON serialization so they work with the expression engine:

type Artifact struct {
    Name    string `json:"name"`
    Version string `json:"version"`
}

artifact := &Artifact{Name: "api", Version: "1.0.0"}
root, _ := core.LoadObject(artifact)
// root is now map[string]any{"name": "api", "version": "1.0.0"}

Slices of structs work the same way:

items := []any{
    &Artifact{Name: "api", Version: "1.0.0"},
    &Artifact{Name: "web", Version: "2.3.1"},
}
root, _ := core.LoadObject(items)

Evaluating Expressions#

The core.Engine provides CEL expression evaluation, navigation, and rendering:

engine, _ := core.New()

// Evaluate a CEL expression
result, err := engine.Evaluate("_.name", root)

// Navigate to a nested path
node, err := engine.NodeAtPath(root, "metadata.labels")

// Get table rows ([][]string) for your own renderer
rows := engine.Rows(root)

// Render a scalar value as a string
s := engine.Stringify(result)

Note: engine.RenderTable() renders a simple two-column KEY/VALUE table. For arrays of objects, use tui.RenderTable() instead — it auto-detects homogeneous arrays and renders them as columnar tables with field-name headers. See Rendering Tables below.

Common CEL expressions#

_.fieldName                          # access a field
_.items[0]                           # array index
_.items.filter(x, x.active)         # filter a list
_.items.map(x, x.name)              # map/transform
_.items.size()                       # list length
_.name.startsWith("api")            # string functions
_.count > 10 ? "high" : "low"       # ternary

Rendering Tables#

Bordered table (matches kvx CLI output)#

output := tui.RenderTable(root, tui.TableOptions{
    AppName:  "my-app",   // shown in the top border
    Path:     "_",        // shown in the bottom border
    Bordered: true,
    // Width: 0,          // 0 = auto-detect terminal width
    // NoColor: false,    // set true for CI/tests
})
fmt.Print(output)

Plain table (no borders)#

output := tui.RenderTable(root, tui.TableOptions{
    Bordered: false,
    NoColor:  true,
})

Array formatting#

Arrays of objects automatically render as multi-column tables. Control this with:

tui.RenderTable(root, tui.TableOptions{
    Bordered:     true,
    ColumnarMode: tui.ColumnarModeAuto,       // "auto" (default), "always", or "never"
    ArrayStyle:   tui.ArrayStyleNumbered,      // "numbered" (default), "index", "bullet", "none"
    ColumnOrder:  []string{"name", "version"}, // preferred column order (arrays and single maps)
    HiddenColumns: []string{"sha"},            // columns to omit
})

Column display hints (schema)#

Control column headers, widths, alignment, and visibility using ColumnHints. You can build hints programmatically or parse them from a JSON Schema:

// Option 1: Build hints directly
hints := map[string]tui.ColumnHint{
    "user_id":   {DisplayName: "ID", MaxWidth: 10},
    "full_name": {DisplayName: "Name"},
    "score":     {Align: "right"},           // right-align numbers
    "internal":  {Hidden: true},             // hide this column
}

output := tui.RenderTable(root, tui.TableOptions{
    Bordered:    true,
    ColumnOrder: []string{"full_name", "user_id", "score"},
    ColumnHints: hints,
})
// Option 2: Parse from JSON Schema
schemaJSON := []byte(`{
    "type": "array",
    "items": {
        "type": "object",
        "required": ["id", "name"],
        "properties": {
            "id":    {"title": "ID", "type": "string", "maxLength": 10},
            "name":  {"title": "Name", "type": "string"},
            "price": {"type": "number"},
            "legacy": {"deprecated": true}
        }
    }
}`)

hints, err := tui.ParseSchema(schemaJSON)
if err != nil {
    log.Fatal(err)
}

output := tui.RenderTable(root, tui.TableOptions{
    Bordered:    true,
    ColumnHints: hints,
})

ParseSchema derives hints from standard JSON Schema fields:

JSON Schema FieldColumnHint Effect
titleDisplayName — column header text
maxLengthMaxWidth — cap column width
enumMaxWidth — longest enum value
format (date, uuid, etc.)MaxWidth — auto-calculated
type: integer/numberAlign: "right"
deprecated: trueHidden: true
required arrayPriority boost

Flex columns (fill terminal width)#

By default, bordered tables shrink to fit the natural content width. When you want one or more columns to absorb remaining terminal space (e.g. a “message” or “description” column), mark them with Flex: true:

hints := map[string]tui.ColumnHint{
    "severity": {MaxWidth: 8, Priority: 10},
    "message":  {Flex: true},  // absorbs remaining terminal width
}

output := tui.RenderTable(root, tui.TableOptions{
    Bordered:    true,
    ColumnHints: hints,
})

Key behaviors:

  • When any column has Flex: true, the bordered table fills the full terminal width instead of shrinking to content.
  • Surplus space is distributed evenly among flex columns.
  • MaxWidth acts as a minimum guarantee for flex columns during initial sizing, not a cap on expansion. Flex columns start at MaxWidth (if set) and grow to fill remaining space.
  • Non-flex columns keep their natural (content-fitted) width.
  • If no column is marked Flex, behavior is unchanged (fit-to-content).

Calculating table overhead#

In most cases you do not need to calculate overhead manually – use Flex: true and let kvx handle the layout math internally.

For advanced use cases where you need to compute available content width (e.g. to set a dynamic MaxWidth), the overhead formula is:

overhead = 2 (borders) + (numCols - 1) * 2 (separators)

Add the row-number column width if row numbers are enabled.

Unified Render function#

If you want to let the caller choose the output format at runtime (table, list, YAML, JSON), use tui.Render with an OutputFormat:

// format could come from a flag, config, etc.
format := tui.FormatTable // or FormatList, FormatYAML, FormatJSON

output := tui.Render(root, format, tui.TableOptions{
    Bordered: true,
    NoColor:  true,
})
fmt.Print(output)

Available formats:

ConstantOutput
tui.FormatTableColumnar table (default)
tui.FormatListVertical property list
tui.FormatYAMLYAML
tui.FormatJSONIndented JSON

Interactive TUI#

Basic launch#

cfg := tui.DefaultConfig()
cfg.AppName = "my-app"

if err := tui.Run(root, cfg); err != nil {
    log.Fatal(err)
}

Configuration options#

tui.DefaultConfig() returns sensible defaults. Override only what you need:

cfg := tui.DefaultConfig()

// Appearance
cfg.AppName  = "my-app"     // header title
cfg.ThemeName = "warm"       // built-in themes: "dark", "warm", "cool"
cfg.NoColor   = true         // disable colors

// Behavior
cfg.InitialExpr = "_.items"  // start with an expression pre-evaluated

// UI text
cfg.KeyHeader        = "FIELD"           // table header (default: "KEY")
cfg.ValueHeader      = "DATA"            // table header (default: "VALUE")
cfg.InputPlaceholder = "Type a path…"    // input bar placeholder

// Feature toggles (pass *bool)
f := false
cfg.AllowFilter      = &f  // disable search/filter (F3)
cfg.AllowSuggestions = &f  // disable autocomplete

See tui.Config for the full list of fields.

Snapshot mode (non-interactive)#

Render exactly what the TUI would show, then exit — useful for CI or scripted output:

output := tui.RenderSnapshot(root, cfg)
fmt.Print(output)

Custom CEL Functions#

Add your own functions to the expression engine so they appear in TUI suggestions:

import (
    "github.com/google/cel-go/cel"
    "github.com/google/cel-go/common/types"
    "github.com/google/cel-go/common/types/ref"
    "github.com/oakwood-commons/kvx/pkg/tui"
)

// 1. Build a CEL environment with your functions.
env, _ := cel.NewEnv(
    cel.Variable("_", cel.DynType),
    cel.Function("double",
        cel.Overload("double_int",
            []*cel.Type{cel.IntType}, cel.IntType,
            cel.FunctionBinding(func(args ...ref.Val) ref.Val {
                return args[0].(types.Int) * 2
            }),
        ),
    ),
)

// 2. Create a provider (optional hints show in autocomplete).
provider := tui.NewCELExpressionProvider(env, map[string]string{
    "double": "e.g. double(5) → 10",
})

// 3. Plug it in before launching the TUI.
tui.SetExpressionProvider(provider)

cfg := tui.DefaultConfig()
tui.Run(root, cfg)

Concurrency#

tui.RenderTable/tui.RenderList and core.Engine.Rows/RenderTable are safe to call from multiple goroutines concurrently, including with different per-call options (theme, MaxValueLines): each call builds its own render state instead of mutating shared package globals. Sort order is per-call-safe through core.Engine.Rows (which threads an explicit SortOrder down), but tui.RenderTable/RenderList have no per-call sort-order option yet – they still read the process-global default via navigator.SetSortOrder, so concurrent callers wanting different orders through pkg/tui specifically cannot get them (see the global-defaults caveat below).

The package-level configuration setters — tui.SetMaxValueLines, ui.SetTheme/SetThemeByName, navigator.SetSortOrder, and the legacy formatter.SetTableTheme/SetMaxValueLines — still change process-wide defaults. They are internally synchronized (no data races or panics), but concurrent callers changing these defaults will race for “whose setting wins” for anything that reads the global default rather than an explicit per-call option. If you render concurrently with different settings per call, pass them explicitly (e.g. TableOptions.MaxValueLines) rather than relying on these global setters.

Working Examples#

ExampleDescriptionRun
examples/embed-tuiLoad a file, render a bordered table or launch the TUIgo run ./examples/embed-tui sample.json
examples/custom_structLoad Go structs, evaluate CEL expressionsgo run ./examples/custom_struct
examples/core-cliHeadless CLI — load, evaluate, render (no TUI)go run ./examples/core-cli data.yaml "_.items[0]"

API Quick Reference#

pkg/core#

Function / MethodDescription
core.LoadFile(path)Load JSON/YAML/NDJSON/JWT from disk
core.LoadRoot(input)Parse a string
core.LoadRootBytes(data)Parse bytes
core.LoadObject(value)Wrap a Go value (map, slice, struct)
core.New(opts...)Create an Engine with defaults
engine.Evaluate(expr, root)Run a CEL expression
engine.NodeAtPath(root, path)Navigate to a nested node
engine.Rows(node)Convert a node to [][]string rows
engine.RenderTable(node, noColor, keyW, valW, columnOrder)Render a plain KEY/VALUE table (no columnar detection – use tui.RenderTable for arrays)
engine.Stringify(node)Render a scalar as a display string

pkg/tui#

FunctionDescription
tui.Run(root, cfg, opts...)Launch the interactive TUI
tui.Render(node, format, opts)Render using an OutputFormat (FormatTable, FormatList, FormatTree, FormatMermaid, FormatYAML, FormatJSON)
tui.RenderTable(node, opts)Render a static table (bordered or plain; auto-detects columnar mode for arrays)
tui.RenderList(node, opts)Render a vertical list (properties stacked per object, like -o list)
tui.RenderTree(node, opts)Render an ASCII tree structure (like -o tree)
tui.RenderMermaid(node, opts)Render a Mermaid flowchart diagram (like -o mermaid)
tui.RenderSnapshot(root, cfg)Render a full TUI frame as a string
tui.DefaultConfig()Get baseline TUI configuration
tui.DetectTerminalSize()Get terminal width and height
tui.NewCELExpressionProvider(env, hints)Create an expression provider from a CEL env
tui.SetExpressionProvider(p)Override the global expression provider
tui.ResetExpressionProvider()Restore the default provider

tui.ColumnHint fields#

FieldTypeDescription
MaxWidthintCap column width (0 = no cap). For flex columns, acts as a minimum guarantee — the column starts at MaxWidth but can expand beyond it
PriorityintColumn importance during shrinking (higher = resists more)
DisplayNamestringOverride column header text
Alignstring"right" or "left" (default)
HiddenboolOmit column from output
FlexboolAbsorb remaining terminal width after fixed columns. Auto-set by ParseSchema for columns without maxLength/enum/format constraints

internal/formatter (advanced)#

FunctionDescription
formatter.ColumnarOverhead(numCols, showRowNum, numRows)Calculate fixed overhead (borders + separators + row numbers)
formatter.HasFlexColumn(hints)Check if any hint has Flex: true