ntcharts-svg

module
v0.2.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 28, 2026 License: MIT

README

ntcharts-svg — Terminal SVG viewer & vector canvas for Bubble Tea

Latest Release GoDoc Code Of Conduct

ntcharts-svg is a Bubble Tea widget that makes SVG documents first-class citizens in terminal UIs (and WASM/browser builds). It pairs oksvg + rasterx for pure-Go SVG rasterization with ntcharts/v2/picture for image rendering — half-block glyphs anywhere, full-resolution Kitty graphics on terminals that support them (Kitty, Ghostty, WezTerm).

Try out the live WASM demo.

It is the sibling of ntcharts-pdf and mirrors its architecture. The key difference: because the SVG rasterizer is pure Go, image rendering needs no CGO, no system dependencies, and no JS bridge — it works identically on native, browser-WASM, and WASI builds.

The widget does double duty:

  • View existing SVGs — load any .svg file or in-memory bytes, rasterize, zoom, and pan.
  • Generate vector graphics — draw with an immediate-mode Canvas and chart helpers, export clean SVG (or PNG), and display the result in the same viewer.

Quickstart

package main

import (
    "fmt"
    "os"

    tea "charm.land/bubbletea/v2"
    "github.com/NimbleMarkets/ntcharts-svg/svg"
)

type model struct{ sv svg.Model }

func (m model) Init() tea.Cmd { return m.sv.Init() }

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    if k, ok := msg.(tea.KeyMsg); ok && (k.String() == "q" || k.String() == "ctrl+c") {
        return m, tea.Quit
    }
    if sz, ok := msg.(tea.WindowSizeMsg); ok {
        return m, m.sv.SetSize(sz.Width, sz.Height)
    }
    var cmd tea.Cmd
    m.sv, cmd = m.sv.Update(msg)
    return m, cmd
}

func (m model) View() tea.View { return m.sv.View() }

func main() {
    sv := svg.NewWithConfig(svg.Config{InitialPath: os.Args[1]})
    if _, err := tea.NewProgram(model{sv: sv}).Run(); err != nil {
        fmt.Println(err); os.Exit(1)
    }
}

Toggle Raster ↔ Info with m, swap Glyph ↔ Kitty with g, zoom with + / -, pan with arrows, cycle fit with f, reload with r, reset view with 0. The widget's Update dispatches all of these through DefaultKeyMap(); rebind a field to change the keystroke (or set a binding to key.Binding{} to disable it).

Demo

A fuller demo lives at examples/svgview — adds a status bar, help bubble, and the c / e keys that generate and export a chart at runtime.

task build-ex-svgview
./bin/ntcharts-svgview path/to/your.svg   # or no argument for the embedded sample

The same example compiles to WebAssembly and runs in the browser via booba — no JS bridge required, since the rasterizer is pure Go. Build and serve it locally:

task serve-wasm-site   # builds web/app.wasm, stages booba assets, serves at :8000

task build-wasm-site alone produces the deployable web/ directory; the .github/workflows/pages.yml workflow publishes it to GitHub Pages on every push to main.

Modes

Mode What it does Where it works
svg.RasterMode (default) Rasterizes the SVG via a Renderer (default: oksvg/rasterx), fed to ntcharts/v2/picture. Supports zoom (+/- up to ×64) and pan (arrow keys when zoomed) Everywhere — native, browser-WASM, WASI
svg.InfoMode Zero-dependency textual summary: view box, declared size, element-type histogram, embedded <title> / <desc> Everywhere

sv.ToggleMode() returns a tea.Cmd that swaps modes and re-renders. RasterMode internally uses picture.PictureGlyph (universal half-blocks) or picture.PictureKitty (high-resolution); switch between them with sv.ToggleRenderMode().

Unlike ntcharts-pdf, RasterMode is the default — the rasterizer is pure Go and always available, so there is no platform where image rendering silently degrades.

Renderer

type Renderer interface {
    Render(maxW, maxH int) (image.Image, error)
    ViewBox() (w, h float64)
    Close() error
}

type RendererFactory func(name string, data []byte) (Renderer, error)

The factory is bytes-first: the widget reads file contents once (subject to Limits.MaxFileBytes) and hands the bytes to the factory, so custom factories never have to implement their own file I/O. Hosts that already have the bytes — an //go:embed blob, an HTTP response, a generated Canvas — skip the path round-trip via sv.SetSVGData(name, data) or Config.InitialData / InitialName.

When Config.RendererFactory is nil, NewWithConfig installs DefaultRendererFactory(), backed by oksvg/rasterx. Bring-your-own renderers (resvg via CGO, a server-side rasterizer, a caching layer) plug in through the same interface. Config.RenderEdge (default 1024) sets the longer-edge resolution of the rasterized bitmap; zoom then crops into that bitmap.

Loading APIs
Call When to use
sv.SetSVG(path) / Config.InitialPath SVG is on disk; widget reads it once (Limits.MaxFileBytes enforced via os.Stat)
sv.SetSVGData(name, data) / Config.InitialData + InitialName SVG bytes already in memory (embedded, fetched, generated)
sv.ShowCanvas(c) Display a *svg.Canvas you drew with the immediate-mode API
sv.SetImage(img) Install a pre-rasterized image.Image, bypassing the Renderer entirely
Renderer state introspection
  • sv.HasRenderer() bool — true when the factory produced a Renderer for the current document.
  • sv.RendererErr() error — the most recent factory error, persisted for the document's lifetime (cleared on the next load). InfoMode still works; surface this to explain why RasterMode is unavailable.

Immediate-mode Canvas

Canvas is a self-contained, pure-Go SVG writer — draw with chainable primitives, then export clean SVG, a PNG, or an image.Image.

c := svg.NewCanvas(200, 120).Background("white")
c.Circle(60, 60, 40, svg.Fill("#4e79a7"))
c.Line(0, 0, 200, 120, svg.Stroke("#e15759", 3))
c.Text(100, 110, "hello", svg.Paint{}.WithFont("sans-serif", 12).WithAnchor("middle"))

svgText := c.ToSVG()              // standalone SVG markup
_ = c.WriteSVG("out.svg")          // write a .svg file
_ = c.WritePNG("out.png", 800, 600) // rasterize to PNG
img, _ := c.ToImage(800, 600)      // rasterize to image.Image

Primitives: Line, Rect, RoundRect, Circle, Ellipse, Polyline, Polygon, Path, Text, and Raw for an escape hatch. Paint carries fill / stroke / opacity / dash / font, with fluent With* builders and the Fill / Stroke constructors.

Chart helpers

Chart builds bar and line charts on top of the Canvas:

canvas := svg.NewChart(360, 240).
    SetTitle("Quarterly Revenue").
    SetLabels("Q1", "Q2", "Q3", "Q4").
    AddSeries("2025", "", 12, 19, 14, 23).
    AddSeries("2026", "", 17, 15, 21, 28).
    BarCanvas() // or LineCanvas()

cmd := mySvgModel.ShowCanvas(canvas) // render it straight into the viewer

This is the "generate chart → SVG → display" loop the c key in the example demonstrates.

Resource limits (Config.Limits)

SVG is an untrusted-input format; a hostile document can nest elements millions deep, inline a gigabyte of base64 image data, or declare a viewBox that projects to a terabyte bitmap. Config.Limits exposes conservative caps applied automatically:

Field Default What it caps
MaxFileBytes 64 MiB SVG size before the loader reads it
MaxElements 250 000 Element-histogram counting; the scan stops and reports "capped"
MaxRenderPixels 100 000 000 Rasterized bitmap area; the renderer scales down to fit
MaxRenderEdge 8192 Either edge of the rasterized bitmap

Set any field to -1 to disable that cap for trusted input; zero uses the default. SVG parsing and rasterization are both panic-guarded — a malformed document surfaces an error rather than crashing the load goroutine.

Sanitizing SVG-derived strings

SVG titles, descriptions, element names, and error messages can carry C0/C1 control sequences and Unicode bidi format chars ("Trojan Source"-style attacks). The widget's own status text is already sanitized. For host-displayed strings derived from the SVG — Model.Title(), Model.Name() for in-memory docs, RendererErr().Error() — use svg.SanitizeForTerminal(s string) string, which drops anything outside Unicode's printable categories and folds newlines / tabs to single spaces.

Known caveats

  • Zoom is vector-sharp. With a document Renderer attached, ZoomIn / PanLeft (etc.) re-rasterize the viewport sub-rectangle via Renderer.RenderRegion, so zoomed-in views stay crisp at any depth rather than upscaling a fixed bitmap. Zoom 0 reuses the full-document bitmap (no re-render); each zoom/pan keypress at depth costs one rasterize. A host bitmap installed via SetImage has no renderer, so it still crops (chunky-pixel inspection) — there are no vectors to re-rasterize.
  • oksvg supports a broad but not exhaustive slice of SVG. Filters, advanced gradients, embedded fonts, and CSS-heavy documents may render approximately or skip elements (the parser runs in ignore-errors mode). For pixel-perfect fidelity, wire a custom RendererFactory around a stricter rasterizer.
  • The immediate-mode Canvas is a writer, not a layout engine. It emits exactly the elements you draw. Text is not measured — text-anchor positions runs, but you supply the coordinates. This keeps Canvas dependency-free and WASM-safe.
  • WriteSVG / WritePNG do filesystem I/O and will fail at runtime under GOOS=js. Use ToSVG / Bytes / ToImage in the browser.
  • Kitty placement composition through lipgloss is best-effort. picture.Model.View() embeds the Kitty placement escapes directly in the rendered string, which composes cleanly inside lipgloss.JoinVertical and basic borders but may misbehave under styles that rewrite the content. Glyph mode has no such caveat.
  • Vibe coded. This comment was inserted by a human.

License

Thanks to the oksvg and rasterx libraries and their authors. We are currently using [a fork]](https://github.com/NimbleMarkets/oksvg/tree/nm) of their oksvg library.

MIT License — Copyright (c) 2026 Neomantra Corp.


Made with ❤ and 🔥 by the team behind Nimble.Markets.

Directories

Path Synopsis
examples
svgview command
examples/svgview/main.go — single-pane svgview demo.
examples/svgview/main.go — single-pane svgview demo.
Package svg is a Bubble Tea v2 widget that makes SVG documents first-class citizens in terminal UIs (and WASM/browser builds).
Package svg is a Bubble Tea v2 widget that makes SVG documents first-class citizens in terminal UIs (and WASM/browser builds).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL