Write your data once. Render it anywhere.
One library. Sixteen formats. Three data shapes. Zero lock-in.
📚 Documentation · 🚀 Quick Start · 📋 Format Matrix
go-output is a Go library that turns your structured data into 16 output formats — tables, trees, and diagrams — with type-safe enums, branded IDs, and zero-config color support. It also includes NOM-style real-time progress visualization for long-running workflows, inspired by nix-output-monitor.
import "github.com/larsartmann/go-output"The root module has zero heavy dependencies — only golang.org/x/term. YAML, lipgloss, bubbletea, and diagram renderers live in isolated sub-modules you import only when you need them.
Requires Go 1.26+.
Build tabular data once, render it in any format:
import (
"os"
"github.com/larsartmann/go-output"
"github.com/larsartmann/go-output/delimited"
"github.com/larsartmann/go-output/markdown"
"github.com/larsartmann/go-output/serialization"
)
// Define your data once
data := output.NewTable([]string{"Name", "Health", "Complexity"})
data.AddRow([]string{"Alpha", "90%", "7/10"})
data.AddRow([]string{"Beta", "75%", "5/10"})
data.SetFooter([]string{"Total", "2", "-"})
// Render to Markdown
md := markdown.NewMarkdownTable()
md.SetHeaders(data.GetHeaders())
for _, row := range data.GetRows() { md.AddRow(row) }
out, _ := md.Render()
// Or serialize to JSON
jtr := serialization.NewJSONTableRenderer()
jtr.SetData(data)
out, _ = jtr.Render()
// Or stream to CSV
csv := delimited.NewCSVWriter(os.Stdout)
_ = csv.WriteHeader(data.GetHeaders())
for _, row := range data.GetRows() { _ = csv.WriteRow(row) }
csv.Flush()Sub-module imports (e.g.,
delimited,markdown,serialization) require cloning the repo and setting up the workspace — see Installation. Root-only usage works with justgo get.
Use the Format enum for runtime format selection — perfect for CLI flags:
format, _ := output.ParseFormat("json") // validates input
fmt.Println(format.Supports(output.ShapeTable)) // true
fmt.Println(format.Shapes()) // [table tree graph]Or dispatch through the unified renderer:
output.RenderTable(data, output.FormatHTML, output.RenderOptions{
ColorMode: output.ColorModeAuto,
})v0.30.0 introduces a CQRS (Command-Query Responsibility Segregation) architecture: builders are mutable, snapshots are immutable, and renderers are pure functions.
import (
"github.com/larsartmann/go-output"
"github.com/larsartmann/go-output/graph"
)
// 1. Build — mutable
b := output.NewGraphBuilder()
b.AddNode(*output.NewGraphNode("compile", "Compile"))
b.AddNode(*output.NewGraphNode("test", "Test"))
b.AddEdge(*output.NewGraphEdge("compile", "test"))
// 2. Freeze — immutable snapshot
g := b.Build()
// 3. Render — pure functions, same Graph, multiple formats
dot, _ := graph.RenderDOT(g)
mermaid, _ := graph.RenderMermaid(g)The same pattern works for tables and trees:
// Table
tbl := output.NewTableBuilder().
SetHeaders("Name", "Status").
AddRow("Compile", "done").
Build()
csv, _ := delimited.RenderCSV(tbl)
// Tree
root := output.NewTreeBuilder().
SetRoot("build", "Build").
AddChild("build", "compile", "Compile").
Build()
ascii, _ := tree.RenderASCII(root)Cross-shape projections convert between data shapes as pure functions:
g := output.TableToGraph(tbl) // Table → Graph
t := output.GraphToTree(g) // Graph → Tree- 16 formats, one API — Same
Table,TreeNode, orGraphNode. No format-specific code paths. - Type-safe everything —
Format,ColorMode,ActivityStatus— all validated at parse time. Branded IDs prevent mixingD2NodeIDwithTreeNodeIDat compile time. - Zero heavy deps in root —
go get go-outputpulls onlyx/term. YAML, lipgloss, bubbletea, and diagram renderers are opt-in sub-modules. - NOM real-time progress — Dependency trees, activity counts, timing estimates, and inline terminal rendering. O(1) summary bars even at 10,000 activities.
- Streaming for large data —
StreamingHTMLRendererwrites incrementally with minimal memory. - Zero-config color —
ColorModeAutodetects TTY, respectsNO_COLOR,CI,FORCE_COLOR. - API stable — ADR 006 locks core interfaces. Breaking changes are documented and versioned.
Same data, rendered six ways — all from one Table:
Markdown table:
| Name | Health | Complexity |
|-------|--------|------------|
| Alpha | 90% | 7/10 |
| Beta | 75% | 5/10 |
| Gamma | 85% | 8/10 |
CSV:
Name,Health,Complexity
Alpha,90%,7/10
Beta,75%,5/10
Gamma,85%,8/10
TOTAL,3,-
Tree:
└── Projects
├── Alpha (health: 90%, complexity: 7)
├── Beta (health: 75%, complexity: 5)
└── Gamma (health: 85%, complexity: 8)
D2 diagram:
projects: {
shape: sql_table
id: serial {constraint: primary_key}
name: varchar(255)
health: int
}
Alpha: { shape: circle }
projects -> Alpha { target-arrowhead.shape: cf-many }
Mermaid flowchart:
flowchart TD
Alpha[Alpha]
Beta[Beta]
Alpha --> Beta
DOT / Graphviz:
digraph G {
rankdir=LR;
"Alpha" [label="Alpha"];
"Beta" [label="Beta"];
"Alpha" -> "Beta";
}
Run go run ./examples/basic <format> to see them all.
go get github.com/larsartmann/go-outputSub-modules (diagram renderers, progress visualization, etc.) are build-boundary optimizations within this repo — they are NOT independently go get-able. To use them, clone the repo and set up the workspace:
git clone https://github.com/larsartmann/go-output.git
cd go-output
nix run .#setup-workspace # generates go.work from go.work.exampleThen import what you need. The replace directives in each module's go.mod resolve siblings locally. See ADR 009 for the rationale.
| What you want | Import path |
|---|---|
| Core types, enums, registries | github.com/larsartmann/go-output |
| CSV + TSV writers | github.com/larsartmann/go-output/delimited |
| JSON + YAML + TOML + JSONL | github.com/larsartmann/go-output/serialization |
| XML + HTML + AsciiDoc | github.com/larsartmann/go-output/markup |
| Terminal tables (lipgloss) | github.com/larsartmann/go-output/table |
| Markdown tables | github.com/larsartmann/go-output/markdown |
| ASCII trees | github.com/larsartmann/go-output/tree |
| D2 diagrams | github.com/larsartmann/go-output/d2 |
| DOT + Mermaid | github.com/larsartmann/go-output/graph |
| PlantUML | github.com/larsartmann/go-output/plantuml |
| NOM-style progress | github.com/larsartmann/go-output/nom |
| Bubble Tea TUI | github.com/larsartmann/go-output/tui |
| Format | Table | Tree | Graph | Module | Notes |
|---|---|---|---|---|---|
json |
✅ | ✅ | ✅ | root | Shape-agnostic serialization |
yaml |
✅ | ✅ | ✅ | serialization | Shape-agnostic serialization |
toml |
✅ | ✅ | ✅ | serialization | Shape-agnostic serialization |
csv |
✅ | delimited | Streaming writer with auto-quoting | ||
tsv |
✅ | delimited | Tab-separated with type-switch marshaling | ||
jsonl |
✅ | serialization | One JSON object per line | ||
xml |
✅ | markup | Structured <table> with XML escaping |
||
html |
✅ | ✅ | markup | Styled tables + collapsible trees | |
asciidoc |
✅ | markup | |=== borders with pipe escaping |
||
markdown |
✅ | markdown | Auto column widths, alignment, bold headers | ||
table |
✅ | table | Lipgloss terminal tables with rounded borders | ||
tree |
✅ | tree | ASCII box-drawing (├──, └──) with color cycling |
||
d2 |
✅ | ✅ | d2 | SQL tables, 20 node shapes, grid layouts, style classes | |
mermaid |
✅ | ✅ | graph | Flowcharts with 8 node shapes | |
dot |
✅ | ✅ | graph | Graphviz directed graphs | |
plantuml |
✅ | ✅ | plantuml | Component diagrams with Table→graph conversion |
// Check what a format can do
format, _ := output.ParseFormat("d2")
format.Supports(output.ShapeTable) // true (SQL tables)
format.Supports(output.ShapeGraph) // true (node-edge diagrams)
format.Supports(output.ShapeTree) // false
// Find all formats for a shape
for _, f := range output.FormatsForShape(output.ShapeGraph) {
fmt.Println(f) // json, yaml, d2, mermaid, dot
}// Terminal table with lipgloss styling (requires go-output/table)
tbl := table.New()
tbl.SetHeaders("Name", "Health")
tbl.AddRow("Alpha", "90%")
out, _ := tbl.Render()
// Markdown table (requires go-output/markdown)
md := markdown.NewMarkdownTable()
md.SetHeaders([]string{"Name", "Health"})
md.AddRow([]string{"Alpha", "90%"})
out, _ = md.Render()
// JSON table (array of objects — requires go-output/serialization)
jtr := serialization.NewJSONTableRenderer()
jtr.SetHeaders([]string{"Name", "Health"})
jtr.AddRow([]string{"Alpha", "90%"})
out, _ = jtr.Render()
// [{"Name": "Alpha", "Health": "90%"}]
// CSV streaming writer (requires go-output/delimited)
csv := delimited.NewCSVWriter(os.Stdout)
_ = csv.WriteHeader([]string{"Name", "Value"})
_ = csv.WriteRow([]string{"Item", "123"})
csv.Flush()
// HTML table with footer (requires go-output/markup)
ht := markup.NewHTMLRenderer()
ht.SetHeaders([]string{"Name", "Value"})
ht.AddRow([]string{"Item", "123"})
ht.SetFooter([]string{"Total", "1"})
out, _ = ht.Render()Set Table.Footer for an optional totals/summary row:
data := output.NewTable([]string{"Name", "Score"})
data.AddRow([]string{"Alice", "95"})
data.AddRow([]string{"Bob", "87"})
data.SetFooter([]string{"Total", "182"})| Format | Footer | Behavior |
|---|---|---|
table |
✅ | Bold-styled footer row |
markdown |
✅ | Second separator + bold footer row |
csv / tsv |
✅ | Appended as last data row |
html |
✅ | <tfoot> section with footer-cell class |
xml |
✅ | <footer> element |
asciidoc |
✅ | Footer row cells |
json / yaml / toml / jsonl |
❌ | Data serialization — footer skipped |
tree / d2 / mermaid / dot / plantuml |
❌ | Non-tabular formats |
root := output.NewTreeNode("root", "Projects")
root.AddChild(output.NewTreeNode("alpha", "Alpha"))
root.AddChild(output.NewTreeNode("beta", "Beta"))
// ASCII tree with box-drawing chars (requires go-output/tree)
tree := tree.NewASCIITreeRenderer()
tree.SetRoot(root)
tree.SetColorMode(output.ColorModeAuto)
out, _ := tree.Render()
// Projects
// ├── Alpha
// └── Beta
// JSON tree (requires go-output/serialization)
jtr := serialization.NewJSONTreeRenderer()
jtr.SetRoot(root)
out, _ = jtr.Render()
// HTML collapsible tree (requires go-output/markup)
ht := markup.NewHTMLTreeRenderer()
ht.SetRoot(root)
out, _ = ht.Render()import (
"github.com/larsartmann/go-output"
"github.com/larsartmann/go-output/d2"
"github.com/larsartmann/go-output/graph"
)
nodes := []output.GraphNode{
output.NewGraphNode("a", "API Gateway"),
output.NewGraphNode("b", "Backend"),
}
edges := []output.GraphEdge{
output.NewGraphEdge("a", "b"),
}
// DOT / Graphviz (requires go-output/graph)
dot := graph.NewDOTRenderer()
dot.SetNodes(nodes)
dot.SetEdges(edges)
out, _ := dot.Render()
// Mermaid flowchart (requires go-output/graph)
mmd := graph.NewMermaidRenderer()
mmd.SetNodes(nodes)
mmd.SetEdges(edges)
out, _ = mmd.Render()
// D2 diagrams with SQL tables (requires go-output/d2)
diagram := d2.NewDiagram().
AddNodeWithShape("api", "API Gateway", d2.ShapeHexagon).
AddEdgeSimple("api", "backend")
out, _ = diagram.Render()SQL tables, constraints, grid layouts, and nested containers:
table := d2.Table{
Name: "users",
Columns: []d2.Column{
{Name: "id", Type: "INT", Constraint: d2.ConstraintPrimary},
{Name: "email", Type: "VARCHAR(255)", Constraint: d2.ConstraintUnique},
},
}
node := d2.Node{
ID: output.NewBrandedID[output.D2NodeIDBrand]("dashboard"),
Label: output.NewBrandedID[output.D2NodeLabelBrand]("Dashboard"),
GridRows: 3,
GridColumns: 2,
}Convert between data shapes without rewriting your data:
// Table → Graph (auto-generates edges between consecutive rows)
dot := graph.NewDOTFromTable(data)
mmd := graph.NewMermaidFromTable(data)
plantuml := plantuml.NewPlantUMLFromTable(data)
d2Diagram := d2.NewD2FromTable(data)
// Table → Tree (hierarchical from tabular data; requires go-output/tree)
tree := tree.TreeRendererFromTable(data)
// Tree → Graph
d2Diagram := d2.NewD2FromTree(root)
dot := graph.NewDOTFromTree(root)
mmd := graph.NewMermaidFromTree(root)
plantuml := plantuml.NewPlantUMLFromTree(root)Track and visualize long-running workflows with dependency trees, inspired by nix-output-monitor.
Build state, take a snapshot, render once — useful for logs, CI output, or testing:
import (
"context"
"fmt"
"time"
"github.com/larsartmann/go-output/nom"
)
ctx := context.Background()
sub := nom.NewNOMSubscriber()
// Fire lifecycle events
sub.OnEvent(ctx, nom.WorkflowStarted{
ID: nom.NewWorkflowID("build"),
Name: nom.NewWorkflowName("Build Project"),
})
sub.OnEvent(ctx, nom.ActivityStarted{
ID: nom.NewActivityID("compile"),
Name: nom.NewActivityName("Compile"),
})
sub.OnEvent(ctx, nom.ActivityStarted{
ID: nom.NewActivityID("test"),
Name: nom.NewActivityName("Run Tests"),
Deps: []nom.ActivityID{nom.NewActivityID("compile")},
})
sub.OnEvent(ctx, nom.ActivityCompleted{
ID: nom.NewActivityID("compile"),
Name: nom.NewActivityName("Compile"),
Duration: 5 * time.Second,
})
// Render a snapshot of the current state
snaps := sub.SnapshotActivities()
fmt.Println(sub.DependencyTree().RenderWithSnapshots(snaps, 20, 0))
// O(1) summary counts
counts := sub.GetActivityCounts()
fmt.Printf("Running: %d, Completed: %d, Failed: %d, Pending: %d\n",
counts.Running, counts.Completed, counts.Failed, counts.Pending)
sub.OnEvent(ctx, nom.WorkflowCompleted{ID: nom.NewWorkflowID("build")})For real-time updates, the InlineRenderer redraws the tree in-place using ANSI escape codes. Events typically arrive from concurrent workers:
sub := nom.NewNOMSubscriber()
renderer := nom.NewInlineRenderer(sub, os.Stdout, 20) // maxHeight 20 lines
ctx := context.Background()
renderer.Start(ctx, 100*time.Millisecond) // redraw every 100ms
defer renderer.Finish(nil)
// In your workers (goroutines):
sub.OnEvent(ctx, nom.ActivityStarted{
ID: nom.NewActivityID("compile"),
Name: nom.NewActivityName("Compile"),
})
// ... do work ...
sub.OnEvent(ctx, nom.ActivityCompleted{
ID: nom.NewActivityID("compile"),
Name: nom.NewActivityName("Compile"),
Duration: 5 * time.Second,
})- Dependency trees — Hierarchical parent/child relationships with UTF-8 box-drawing
- O(1) activity counts — Summary bar updates in constant time, even with 10,000+ activities
- Timing cache — Persists duration history to
~/.cache/nom-timing.csvfor ETA estimates - Progress sub-steps —
ActivityProgressevents render a dim→ messagesub-line beneath each activity - Retry visibility —
ActivityRetryingevents transition failed activities back to running, rendering⟳N (reason) - Estimated remaining time —
EstimatedTotalRemaining()powers a~Xm leftsummary segment from per-activity estimates - Snapshot-based rendering — Race-free: renderers read immutable value copies, not shared pointers
- CI-safe degradation — Auto-detects CI environments; appends frames line-by-line instead of ANSI cursor codes
- Height-pressure collapse — When the tree exceeds
maxHeight, completed children collapse with a⋯ N completedmarker - Per-activity progress — Download bars (
▕████░░░░▏ 45%) and host tags (@host) - Node classes — Root/twig/leaf styling with bold roots for top-level visibility
Export live NOM state as DOT or Mermaid diagrams:
// Project subscriber state into graph nodes/edges
reader := sub.Store()
nodes := reader.Nodes()
edges := reader.Edges()
// Render as DOT
dot := graph.NewDOTRenderer()
dot.SetNodes(nodes)
dot.SetEdges(edges)
out, _ := dot.Render()Full-screen interactive TUI built on Bubble Tea v2, with two display modes:
import (
"context"
"github.com/larsartmann/go-output/nom"
"github.com/larsartmann/go-output/tui"
)
reporter := tui.NewBubbleTeaProgressReporter()
reporter.SetDisplayMode(tui.DisplayModeNOM) // or DisplayModeUniversal
// Optional: wire ctrl+c to cancel a context
ctx, cancel := context.WithCancel(context.Background())
reporter.SetCancelFunc(cancel)
// Report progress
reporter.ReportStep(1, 5, "Building...")
reporter.ReportProgress(0.35)
reporter.ReportMessage("Compiling module Alpha")
// Or drive via NOM events
reporter.Subscriber().OnEvent(ctx, nom.ActivityStarted{
ID: nom.NewActivityID("a1"),
Name: nom.NewActivityName("Build"),
})
reporter.Start()
defer reporter.Stop()| Key | Action |
|---|---|
j / ↓ |
Scroll down |
k / ↑ |
Scroll up |
pgdown |
Scroll half page down |
pgup |
Scroll half page up |
g / Home |
Jump to top |
G / End |
Jump to bottom |
? |
Toggle help overlay |
q |
Quit (workflow continues) |
ctrl+c |
Cancel workflow and quit |
| Mouse | Wheel scroll, click to select nodes |
All configuration types provide validation and conversion:
format, err := output.ParseFormat("json")
if format.IsValid() {
fmt.Println(format.String()) // "json"
fmt.Println(format.Shapes()) // [table tree graph]
}
allowed := format.AllowedValues() // ["table", "json", "csv", ...]Available enums: Format (16 values), Shape (3 values), ColorMode (auto/always/never), NodeShape (7 shapes), d2.NodeShape (20 shapes), d2.ArrowType (11 types), d2.Constraint (3 constraints), Alignment (left/right/center).
Phantom types prevent mixing different ID types at compile time:
nodeID := output.NewBrandedID[output.D2NodeIDBrand]("node-1")
treeID := output.NewBrandedID[output.TreeNodeIDBrand]("root")
// nodeID = treeID // COMPILE ERROR: different branded typesDefine your own:
type ProjectIDBrand struct{}
projectID := output.NewBrandedID[ProjectIDBrand]("proj-123")All terminal renderers support ColorMode for controlling ANSI output:
// Terminal table: functional option
tbl := table.New(table.WithColorMode(output.ColorModeAlways))
// ASCII tree: setter
tree := tree.NewASCIITreeRenderer()
tree.SetColorMode(output.ColorModeAlways)
// Markdown: chaining setter
md := markdown.NewMarkdownTable().SetColorMode(output.ColorModeAlways)
// Unified dispatch: RenderOptions
output.RenderTable(data, output.FormatTable,
output.RenderOptions{ColorMode: output.ColorModeAuto})ColorModeAuto detects TTY via golang.org/x/term, respects NO_COLOR, CI, GITHUB_ACTIONS, GITLAB_CI, JENKINS_URL, BUILDKITE, GO_OUTPUT_FORCE_COLOR, FORCE_COLOR.
For large datasets, stream output incrementally:
data := output.NewTable([]string{"Name", "Value"})
data.AddRow([]string{"Item", "123"})
renderer := markup.NewStreamingHTMLRenderer()
renderer.SetData(data)
_ = renderer.Stream(os.Stdout)The escape/ subpackage provides safe escaping for each format:
import "github.com/larsartmann/go-output/escape"
safe := escape.HTML("<script>alert('xss')</script>")
safeID := escape.D2("my-node.with.dots")| Function | Purpose |
|---|---|
escape.HTML |
HTML special characters |
escape.XML |
XML special characters |
escape.D2 |
D2 diagram identifiers |
escape.DOT |
DOT graph identifiers |
escape.MermaidID |
Mermaid node IDs |
escape.MermaidText |
Mermaid labels |
escape.MermaidSlug |
Mermaid slug fallback |
escape.PlantUML |
PlantUML labels |
escape.SlugifyID |
Cross-format diagram identifiers |
Run the basic example to see all 16 formats:
go run ./examples/basic markdown # auto color
go run ./examples/basic tree --color always # force colors
go run ./examples/basic table --color never # no colorsOther examples:
| Example | What it demonstrates |
|---|---|
examples/basic/ |
All 16 formats with a Project dataset |
examples/cqrs/ |
CQRS Build→Freeze→Render pipeline for all 3 data shapes |
examples/nom_progress/ |
NOM workflow events, dependency trees, activity counts |
examples/nom_inline_renderer/ |
Live InlineRenderer terminal output |
examples/nom_dag/ |
Layered mode, categories, themes, DAG export |
examples/tui_progress/ |
Bubble Tea TUI with step reporting and NOM display mode |
examples/d2/ |
D2 microservice architecture with SQL tables and shapes |
examples/diagram_export/ |
Export NOM live state as DOT/Mermaid diagrams |
nix develop # Enter dev shell (Go 1.26, golangci-lint, gopls)
nix run .#build # Build all 19 modules
nix run .#test # Test all 19 modules
nix run .#test-race # Race-test nom + tui
nix run .#lint # golangci-lint across all modules
nix run .#tidy # go mod tidy all modules
nix run .#govulncheck # Vulnerability scan
nix fmt # Format .nix files
nix flake check # Formatting + pre-commit hooksgo build ./... # Build all workspace modules
go test ./... # Test all modules
go test -race ./... # Race detector
go test -cover ./... # Coverage report
golangci-lint run --fix ./... # LintThis library is pre-v1. The following guarantees apply:
- Root module (
github.com/larsartmann/go-output): Public API is stable. Breaking changes documented in CHANGELOG.md. - Sub-modules (
d2,graph,table,nom,tui, etc.): May evolve independently. Import them explicitly to opt in. Rendererinterface: Stable — all formats implementRender() (string, error).
| Interface | Methods | Implementations |
|---|---|---|
Renderer |
Render() (string, error) |
All 16 formats |
TableRenderer |
SetHeaders([]string), AddRow([]string), Render() |
JSON, YAML, TOML, JSONL, HTML, Streaming HTML, AsciiDoc, Markdown, Table |
TreeRenderer |
SetRoot(*TreeNode), Render() |
ASCII Tree, JSON Tree, YAML Tree, TOML Tree, HTML Tree |
GraphRenderer |
SetNodes([]GraphNode), SetEdges([]GraphEdge), Render() |
D2, DOT, Mermaid, PlantUML, JSON Graph, YAML Graph, TOML Graph |
StreamingRenderer |
Stream(io.Writer) error, Render() |
Streaming HTML |
Non-breaking changes until v1: adding new formats, shapes, methods, sub-modules, and renderers.
19 modules in a multi-module Go workspace. The root package has zero imports of any sub-module — this is the load-bearing architectural guarantee.
go get go-outputpulls no lipgloss, bubbletea, yaml, d2, graph, table, nom, or tui deps.- Sub-modules self-register into root's registries via their own
init(). - Import a sub-module to activate its renderers automatically.
Read docs/FORMAT_ARCHITECTURE.md, docs/DOMAIN_LANGUAGE.md, and docs/adr/ for the full design.
| Old | New |
|---|---|
output.TableData |
output.Table |
output.TableDataStore |
output.TableStore |
output.GraphStyle |
output.NodeStyle |
output.GraphRendererState |
output.GraphBuilder |
output.TreeOutputRenderer |
output.TreeRenderer |
nom.NOMStyleSubscriber |
nom.NOMSubscriber |
d2.D2Diagram |
d2.Diagram |
All d2.D2Xxx types |
d2.Xxx (prefix dropped) |
| Old | New |
|---|---|
output.RenderTableData() |
output.RenderTable() |
nom.NewNOMStyleSubscriber |
nom.NewNOMSubscriber |
All XxxFromTableData() |
NewXxxFromTable() |
The old mutable renderer structs still work but are now implementation detail. The canonical API is pure functions:
// Old (still works)
r := graph.NewDOTFromTable(data)
out, _ := r.Render()
// New CQRS (recommended)
g := output.TableToGraph(data)
out, _ := graph.RenderDOT(g)NodeShapeRect, EdgeStyle.ArrowHead/.ArrowTail, nom.ErrActivityNotFound, nom.TimingFormat, nom.Activity.IsPhase(), StreamingRendererFromRenderer() — see CHANGELOG.md for the full list.
output.RenderTable(data, output.FormatJSON, opts) now uses the same streaming encoders as the CQRS API (json.NewEncoder, yaml.NewEncoder, etc.). The standard encoders append a trailing \n that the old MarshalIndent + Fprint path did not.
If you do exact-byte output comparison (e.g., golden-file tests in your project), add strings.TrimSpace(result) or update your golden files to include the trailing newline. This affects JSON, YAML, TOML, JSONL, CSV, TSV, and XML.
See CONTRIBUTING.md. The full version history is in CHANGELOG.md, long-term direction in ROADMAP.md.