prettyview

package module
v1.1.0-alpha Latest Latest
Warning

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

Go to latest
Published: Jun 11, 2026 License: BSD-3-Clause Imports: 25 Imported by: 0

README

go-fyne-pretty-view

CI codecov Go Reference Go Report Card Go version License

A memory-efficient, virtualized Fyne widget for viewing structured data — JSON, JSONC, XML, HTML, and raw text — in the style of Bruno's response viewer.

JSON and XML views

Contents

Features

  • Syntax highlighting for JSON / JSONC / XML / HTML, with a dark/light palette you can override. (JSONC // and /* */ comments render as their own nodes — visible, searchable, copyable.)
  • Auto-detection of the input format, with a raw-text fallback for anything else (or malformed input).
  • Expand / fold every container, with a collapse summary on folded nodes ({ 38 items }, [ 3 items ], <tag> 5 children).
  • True character-level free-text selection across rows, with exact-substring copy (Ctrl/Cmd+C) and select-all (Ctrl/Cmd+A).
  • Right-click context menu (Copy / Select all) — the standard Fyne pop-up menu, the same one Fyne's own text widgets use.
  • Copy a whole section (subtree) to the clipboard, regardless of fold state.
  • Search with plain or regular-expression matching, case sensitivity, match navigation, and auto-reveal into folded nodes.
  • Soft word-wrap (toggleable): long lines wrap to the viewport width at word boundaries, or scroll horizontally — selection, search, and copy still operate on whole logical lines.
  • Keyboard navigation: arrows scroll (all four), Space/PageDown & PageUp page, Home/End jump to top/bottom; Shift+arrows / Shift+Home/End extend a selection from the caret; Enter toggles the fold on the caret's line; Esc clears the selection; Ctrl/Cmd+F focuses search.
  • Optional, à-la-carte controls: a built-in toolbar (Open, format, expand/collapse, wrap, search) you can enable control-by-control — or drive everything from your own widgets via the public API.

Why it stays small

The widget is built around a hard memory bound: only the rows currently visible in the viewport ever exist as live canvas objects. Everything else lives in a compact, pointer-free, struct-of-arrays model, and selection, search and copy all operate on that model rather than on widgets.

Measured on the included fixtures:

Input Visible rows Live row widgets Heap after scrolling the whole file
big.json (7.5 MB) 440,005 31 ~80–90 MB

The parsed model is roughly 5× to over 7× the source size — about 4.85× for typical pretty-printed JSON (the ~478 KB openapi.json → ~2.2 MB, guarded by TestModelSizeRatio), rising to ~7.1× for documents dominated by short structural lines (the 7.5 MB big.json → ~51 MB), since each line and segment carries a fixed-size record. A single multi-megabyte line is horizontally culled so no individual text texture is ever wider than the viewport (without that, Fyne would try to rasterize a ~1 GB bitmap for the line).

Install

go get github.com/ideaconnect/go-fyne-pretty-view

Requires Go 1.26.4+ and the usual Fyne build dependencies (a C compiler and the OpenGL/X11 headers on Linux).

Fyne compatibility. Built and tested against Fyne v2.7.x (the version pinned in go.mod). Newer Fyne v2 minor releases are expected to work; each Fyne bump arrives as its own reviewable PR (it is excluded from the batched dependency group) and is validated before release. Security reporting is in SECURITY.md.

Quick start

import (
    "fyne.io/fyne/v2/app"
    prettyview "github.com/ideaconnect/go-fyne-pretty-view"
)

func main() {
    a := app.New()
    w := a.NewWindow("viewer")

    pv := prettyview.New()
    pv.SetData(jsonBytes, prettyview.FormatAuto) // or FormatJSON/FormatJSONC/FormatXML/FormatHTML/FormatRaw

    w.SetContent(pv)
    w.ShowAndRun()
}

The widget itself is just the viewer — it has no built-in buttons. Add the optional toolbar (or your own controls) as shown under Controls.

Functionality and how to toggle it

Everything the viewer can do, and how to turn it on or off. The core viewing behaviors are always on; behavior is tuned with construction Options or the matching runtime setters; the on-screen chrome is entirely opt-in.

Viewer behavior
Capability Default How to change it
Syntax highlighting On Always on; recolor with WithTheme / SetTheme (use FormatRaw for plain, unhighlighted text).
Input format Auto-detect WithFormat(f) at build, or SetData(src, f) / Reparse(f) / SetText(s) at runtime.
Expand / fold a node On (click the triangle) Always available; ExpandAll() / CollapseAll() programmatically.
Initial collapse depth Fully expanded (0) WithDefaultCollapseDepth(d) at build, or SetDefaultCollapseDepth(d) at runtime.
Free-text selection & copy On Always on; SelectAll(), SelectedText(), CopySelection(), ClearSelection(), Ctrl/Cmd+A, Ctrl/Cmd+C.
Right-click context menu On Always on (Copy / Select all).
Copy a subtree On demand CopySubtree(byteOffset) bool (any format; copies the pretty-printed subtree). Also a right-click menu item.
Search On demand Search(SearchQuery{Text, Mode, CaseSensitive}), SearchNext(), SearchPrev(), ClearSearch(), SearchStatus(). Tune with WithSearchConfig(...).
Soft word-wrap Off (WrapNone) WithWrap(WrapWord) at build, or SetWrap(WrapWord) / SetWrap(WrapNone) at runtime; Wrap() reads it.
Tab display width 4 WithTabWidth(n).
Indent step (px/level) 16 WithIndentStep(px).
Line-number gutter Off WithLineNumbers() (1-based logical line numbers, drawn from the model — no per-line widgets).
Theme / colors Track the host Fyne theme WithTheme / WithSyntaxColors at build; SetTheme / SetSyntaxColors at runtime.
Keyboard navigation On Always on: arrows scroll, Space/PageUp/PageDown, Home/End; Shift+arrows extend the selection; Enter toggles the caret line's fold; Esc clears.

SearchQuery.Mode is SearchPlain (default) or SearchRegex; matches are capped by SearchConfig.MaxMatches (10 000 by default) and revealed even inside folded nodes.

Built-in controls (all opt-in)

The toolbar is assembled from ToolbarConfig — every control is a Show* flag, so you include exactly the ones you want:

Control Flag Notes
Open file ShowOpen Needs Window (built-in file dialog) or OnOpen (your own handler).
Format selector ShowFormat auto / json / jsonc / xml / html / raw; re-parses the current source.
Expand all / Collapse all ShowExpandCollapse
Word-wrap toggle ShowWrap Highlighted while wrapping is on.
Search bar ShowSearch Find box, prev/next, live match counter.
Ctrl/Cmd+F focuses search set Window Registered when a Window is supplied.

Each control is also available à la carte (NewSearchBar, NewFormatSelect, NewFoldButtons, NewWrapToggle) so you can place it anywhere, and host widgets can stay in sync via SetOnSearchChanged and SetOnDataChanged.

Construction options

pv := prettyview.New(
    prettyview.WithFormat(prettyview.FormatJSON),       // skip auto-detect
    prettyview.WithWrap(prettyview.WrapWord),           // soft-wrap long lines (or WrapNone to scroll, default)
    prettyview.WithDefaultCollapseDepth(3),             // collapse containers at depth 3 and deeper on load
    prettyview.WithIndentStep(16),                      // pixels per nesting level
    prettyview.WithTabWidth(4),
    prettyview.WithLineNumbers(),                        // opt-in line-number gutter
    // WithSearchConfig merges field-by-field: a zero field keeps its default
    // (DebounceFor stays 150ms). Pass a negative DebounceFor to disable coalescing.
    prettyview.WithSearchConfig(prettyview.SearchConfig{MaxMatches: 5000}),
)

NewWithData(src, format, opts...) is the one-shot form that constructs and loads in a single call.

Controls: built-in, your own, or both

The package optionally provides ready-made controls bound to a PrettyView; every control is individually opt-in, so a host app can use the provided ones as-is, disable them and drive the public API from its own widgets, or mix the two.

pv := prettyview.New()

// (a) Drop in the built-in control bar — pick exactly which controls appear.
bar := prettyview.NewToolbar(pv, prettyview.ToolbarConfig{
    ShowOpen:           true,   // "Open…" file dialog (needs Window or OnOpen)
    ShowFormat:         true,   // format selector (re-parses current source)
    ShowExpandCollapse: true,   // Expand all / Collapse all
    ShowWrap:           true,   // soft-wrap toggle
    ShowSearch:         true,   // find box + prev/next + match counter
    Window:             w,      // enables the Open dialog and Ctrl/Cmd+F focus
})
w.SetContent(container.NewBorder(bar, nil, nil, nil, pv))

The built-in controls are icon-only and carry hover tooltips (via fyne-tooltip). Fyne core has no tooltip support, so the tooltips render only if you wrap your window content in a tooltip layer once — otherwise they are simply absent:

import fynetooltip "github.com/dweymouth/fyne-tooltip"

w.SetContent(fynetooltip.AddWindowToolTipLayer(content, w.Canvas()))

Note: fyne-tooltip is a direct dependency of this module, so it is pulled into every consumer's build even if you never construct a toolbar.

// (b) Or omit the toolbar and wire your own controls to the public API. Use
// SearchDebounced (not Search) for per-keystroke input so a burst coalesces into one
// scan — it honors SearchConfig.DebounceFor (set it via WithSearchConfig).
myFind.OnChanged        = func(s string) { pv.SearchDebounced(prettyview.SearchQuery{Text: s}) }
myExpandButton.OnTapped = pv.ExpandAll

prettyview.DefaultToolbarConfig(win) returns a config with every control enabled (pass your fyne.Window so Open and Ctrl/Cmd+F work, or nil to omit those two). À-la-carte constructors let you place individual built-ins anywhere: prettyview.NewSearchBar(pv), prettyview.NewFormatSelect(pv), prettyview.NewFoldButtons(pv), prettyview.NewWrapToggle(pv). To keep host controls in sync, register pv.SetOnSearchChanged(fn) (match counter), pv.SetOnDataChanged(fn) (format), and pv.SetOnSearchRequested(fn) (focus the search box, e.g. on Ctrl/Cmd+F).

Note on the file dialog: the built-in Open uses Fyne's own in-canvas file browser, not the OS-native picker (Fyne draws all its UI on the GL canvas). For a platform-native dialog, set ToolbarConfig.OnOpen to your own picker and feed the bytes to pv.SetData.

Key methods

Method Purpose
SetData(src, format) / SetText(s) load content
Reparse(format) / Source() / Format() re-parse the current bytes / read them back / current format
ExpandAll() / CollapseAll() / SetDefaultCollapseDepth(d) fold control
ExpandTo(byteOffset) bool / ScrollToLine(line) bool reveal & scroll to a node by source offset (any structured format) or to a display line (any format)
SelectAll() / ClearSelection() / SelectedText() selection
CopySelection() / CopySubtree(byteOffset) bool clipboard (CopySubtree copies the pretty-printed subtree for any format)
Search(SearchQuery{...}) / SearchNext() / SearchPrev() / ClearSearch() / SearchStatus() search
SetWrap(WrapWord/WrapNone) / Wrap() soft-wrap long lines to the viewport, or scroll
SetTheme(variant, Theme{...}) / SetSyntaxColors(variant, SyntaxColors{...}) theming (all colors / syntax-only)
SetOnSearchRequested(fn) / SetOnSearchChanged(fn) / SetOnDataChanged(fn) host hooks (focus search, sync counter, sync format)

Theming

The viewer ships a built-in dark/light palette (theme.go), but every color is overridable. The structural colors (foreground, selection, indent guides) default to tracking the host Fyne theme, so an un-themed viewer blends into your app.

import "fyne.io/fyne/v2/theme"

pv := prettyview.New(
    // Override any subset of colors for a variant; nil fields keep the default.
    prettyview.WithTheme(theme.VariantDark, prettyview.Theme{
        Key:         myKeyColor,
        String:      myStringColor,
        Selection:   mySelectionFill,   // free-text selection fill
        Match:       myMatchFill,        // search highlight
        ActiveMatch: myActiveMatchFill,
        IndentGuide: myGuideColor,
    }),
)

// …or just the syntax tokens, or change it at runtime (both compose):
pv.SetSyntaxColors(theme.VariantDark, prettyview.SyntaxColors{Number: myNumberColor})
pv.SetTheme(theme.VariantLight, prettyview.Theme{Selection: myLightSelection})

Theme covers the syntax tokens (Key, String, Number, Bool, Null, Punct, Tag, Attr, Comment) and the structural colors (Foreground, Summary, IndentGuide, Selection, Match, ActiveMatch). SyntaxColors is the token-only shorthand. Overrides merge, so repeated calls accumulate.

Fonts

Fonts in Fyne are an app-wide setting (the theme's Font()), not a per-widget one. The widget itself only renders the viewer body as monospace and otherwise follows whatever theme your app installs — by default Fyne's bundled DejaVu Sans Mono / Noto Sans.

The optional fonttheme subpackage bundles the project's preferred faces — JetBrains Mono for the monospace body and Inter for UI text — and wraps them as a fyne.Theme you install on your app:

import (
    "fyne.io/fyne/v2/app"
    "fyne.io/fyne/v2/theme"
    "github.com/ideaconnect/go-fyne-pretty-view/fonttheme"
)

a := app.New()
a.Settings().SetTheme(fonttheme.New(theme.DefaultTheme()))

fonttheme.New wraps any base theme and overrides only its fonts, so your base theme's colors, sizes, and icons are preserved. The fonts are embedded in the fonttheme package alone — importing the core prettyview widget pulls in no font data, so you only pay for the typefaces if you opt in.

Override individual faces (a nil field keeps the bundled default) via WithFonts. Each weight is its own field, so to swap the monospace face set both Mono and MonoBold — otherwise bold monospace would still render in JetBrains Mono:

a.Settings().SetTheme(fonttheme.New(theme.DefaultTheme(), fonttheme.WithFonts(fonttheme.Fonts{
    Mono:     myMonoRegular, // swap the monospace face (UI text stays Inter)
    MonoBold: myMonoBold,    // set both weights so bold monospace matches
})))

You are never required to use fonttheme: install your own fyne.Theme (or none) and the widget renders with whatever monospace face that theme provides.

Threading

PrettyView follows the usual Fyne widget rule: it is not safe for concurrent use — call its methods (SetData, Search, ExpandAll, the selection and theme mutators, …) on the goroutine that runs the Fyne event loop. To drive it from another goroutine (e.g. after a network fetch), marshal the call with fyne.Do:

go func() {
    data := fetch()
    fyne.Do(func() { pv.SetData(data, prettyview.FormatAuto) })
}()

The widget holds no locks by design; its one internal background task — the search debounce — already marshals back onto the Fyne goroutine.

Stability

The module is pre-1.0 (v0.x, alpha). The exported API of prettyview and fonttheme may change between minor versions; every breaking change is recorded under Changed/Removed in CHANGELOG.md.

As of v1.0.0 the exported surface is frozen under semantic import versioning: additions ship as v1.x, and any breaking change ships under a new major module path (.../v2) — never as a v1.x bump. The frozen surface is pinned by TestExportedSurfaceGolden (testdata/api_surface.txt), so an accidental change to a public signature fails CI.

Demo

go run ./cmd/prettyview-demo               # loads testdata/openapi.json
go run ./cmd/prettyview-demo path/to/file  # or any file

The demo shows both control styles at once: the built-in NewToolbar (Open, format, expand/collapse, wrap, search) used as-is, plus an app-supplied fixture dropdown that drives the public API directly.

Prebuilt binaries are produced by CI for Linux, Windows, and macOS — each is a zip containing the executable alongside the testdata/ fixtures, so the fixture dropdown works as soon as you extract and run it. Tagged versions publish these zips to the project's GitHub Releases page; CI runs also keep them as build artifacts.

Design and documentation

The full, source-grounded architecture (the virtualization invariant, the struct-of-arrays model, the Fenwick fold index, the char-level selection math, and the adversarial risk analysis) lives in docs/DESIGN.md.

File For whom / what
README.md This overview: features, install, usage, API.
CHANGELOG.md Notable changes per release (Keep a Changelog).
STRUCTURE.md The codebase map — every file, the layering, the mental model.
WORKFLOWS.md How to build, run, test, benchmark, and extend (parsers, colors).
docs/DESIGN.md The authoritative architecture + adversarial risk analysis.
docs/PERFORMANCE.md Performance review: hot paths, benchmarks, and the measured deltas.
HUMANS.md Onboarding and contribution guide for people.
AGENTS.md Brief for AI coding agents: invariants to preserve, conventions.
CLAUDE.md Claude Code entry point (points at AGENTS.md).

Contributing

Contributions are welcome — issues and pull requests both. A few things keep the project healthy:

  • Read the briefs first. HUMANS.md is the human onboarding guide; WORKFLOWS.md covers build/run/test/bench and how to add a parser or a color; AGENTS.md lists the non-negotiable invariants.
  • make check must pass. It runs gofmt, go vet (which also forbids internal/ Fyne imports), and go test -race ./.... CI additionally enforces > 90 % coverage, so ship a regression test with each change.
  • Respect the memory invariants. Only viewport-many rows are ever live widgets; selection/search/copy operate on the model, not on widgets; per-row text is horizontally culled. The arena sizes (Node=32 B, Line=24 B, Segment=12 B) are locked by internal/model/sizes_test.go. If a change regresses renderer_test.go or memory_test.go, it's the change that's wrong.
  • See the UI without a display. make shots renders the fixtures to PNGs via Fyne's software painter, so you can verify layout/colors/highlight z-order headlessly.
  • Keep changes milestone-sized and ship the test in the same change.

Sponsorship

This project is maintained on the side and looking for sponsors to keep the modernization moving forward. If your team relies on it, please consider chipping in ❤️ — every contribution helps keep this library alive:

Sponsor on GitHub Buy Me a Coffee

Thank you to everyone who already supports the project! 🙏

Credits and third-party licenses

This repository vendors third-party assets. Their license texts are kept next to the files, and the obligations below are summarized for convenience — the bundled license texts are authoritative.

Toolbar glyphs — Font Awesome Free (open, expand/collapse, wrap-text, search, up/down). The icons are used under the CC BY 4.0 license; © Fonticons, Inc. The SVGs are vendored under icons/fontawesome/ with the full license at icons/fontawesome/LICENSE.txt, and each SVG keeps Font Awesome's original attribution comment. They are recolored to the active theme foreground when the resource is built.

Bundled fonts (optional, fonttheme only) — both under the SIL Open Font License 1.1:

These fonts are embedded only in the fonttheme subpackage; the core widget bundles no fonts.

If your software uses this library

You inherit obligations only for the assets you actually ship:

  • Font Awesome icons (CC BY 4.0). The icons are compiled into every binary that links the widget (they are tiny embedded SVGs). CC BY 4.0 requires attribution — credit "Font Awesome Free" with a link to https://fontawesome.com and to the license. The simplest way to comply is to keep icons/fontawesome/LICENSE.txt in your distribution (or reproduce its attribution notice in your app's about/credits).

  • JetBrains Mono / Inter (SIL OFL 1.1). You incur these obligations only if you import the fonttheme subpackage, which embeds the font files into your binary. The OFL permits bundling and redistribution; it asks that you include the OFL license text with the fonts and not sell the fonts on their own. Keeping the two OFL.txt / LICENSE.txt files (or their text in your credits) satisfies this. If you do not import fonttheme, you ship no fonts and have nothing to attribute here.

If you do not use fonttheme and you reproduce Font Awesome's attribution elsewhere, you can ship without bundling any of these license files — but vendoring them is the easiest path to compliance. For a concrete example, the prebuilt demo zips (which embed both the icons and the fonts) carry these three license texts under a licenses/ folder alongside the binary.

License

This library's own code is licensed under the BSD 3-Clause License (© 2026 IDCT, Bartosz Pachołek). The third-party assets above keep their respective licenses.

Documentation

Overview

Package prettyview provides a memory-efficient, virtualized Fyne widget for viewing structured data — JSON, JSONC, XML, HTML, and raw text — in the style of Bruno's response viewer: syntax highlighting, per-node expand/fold with collapse summaries, copy-subtree, true character-level free-text selection, and incremental search.

The widget is built around a hard memory bound: only the rows currently visible in the viewport ever exist as live fyne.CanvasObjects. Everything else lives in a compact struct-of-arrays document model, and selection, search, and copy all operate on that model rather than on widgets. As a result a multi-megabyte document occupies a small, predictable number of canvas objects regardless of its size.

Threading

PrettyView follows the standard Fyne widget threading model: it is NOT safe for concurrent use. Every method — including the data, fold, selection, and search mutators (SetData, SetText, Reparse, ExpandAll, CollapseAll, ExpandTo, Search, SearchNext/Prev, ClearSearch, SelectAll, ClearSelection, SetTheme, …) — must be called on the goroutine that runs the Fyne event loop (the "main"/Fyne goroutine), exactly like any other Fyne widget. To drive the widget from another goroutine (e.g. after a network fetch), marshal the call with fyne.Do:

go func() {
    data := fetch()
    fyne.Do(func() { pv.SetData(data, prettyview.FormatAuto) })
}()

The widget intentionally holds no locks: all of its state (the document, the fold index, and the selection/search state) is owned by the Fyne goroutine, so single-threaded access is a precondition rather than something guarded at runtime. The only work that runs off that goroutine is the search-debounce timer (time.AfterFunc); it marshals its scan back via fyne.Do and drops superseded scans via a generation counter, so it never touches widget state concurrently.

Stability

Pre-1.0 (v0.x) the exported API may change between minor versions (see CHANGELOG.md). As of v1.0.0 the surface is frozen under semantic import versioning: additions ship as v1.x and any breaking change ships under a new major module path (.../v2). The frozen surface is pinned by TestExportedSurfaceGolden.

Index

Examples

Constants

View Source
const (
	FormatAuto  = model.FormatAuto  // run AutoDetect heuristics
	FormatRaw   = model.FormatRaw   // plain text, split into physical lines
	FormatJSON  = model.FormatJSON  // strict JSON
	FormatJSONC = model.FormatJSONC // JSON with // and /* */ comments
	FormatXML   = model.FormatXML   // XML
	FormatHTML  = model.FormatHTML  // HTML (tolerant)
)

Variables

This section is empty.

Functions

func NewFoldButtons

func NewFoldButtons(pv *PrettyView) fyne.CanvasObject

NewFoldButtons returns an expand-all / collapse-all icon pair bound to pv.

func NewFormatSelect

func NewFormatSelect(pv *PrettyView) fyne.CanvasObject

NewFormatSelect returns a format selector bound to pv. Choosing a format re-parses the current source; the selection follows the document when content is loaded elsewhere (it registers PrettyView's data-changed hook).

func NewSearchBar

func NewSearchBar(pv *PrettyView) fyne.CanvasObject

NewSearchBar returns a find box (with case-sensitive and regex toggles, prev/next, and a self-updating match counter) bound to pv. It registers PrettyView's search-changed and search-requested hooks so the counter stays in sync and Ctrl/Cmd+F focuses it. Enter finds next, Shift+Enter finds previous, Esc clears.

func NewToolbar

func NewToolbar(pv *PrettyView, cfg ToolbarConfig) fyne.CanvasObject

NewToolbar builds an optional control bar bound to pv from cfg. Disabled controls are omitted. The result is a plain fyne.CanvasObject; place it where you like (typically the top of a container.NewBorder around the PrettyView).

func NewWrapToggle

func NewWrapToggle(pv *PrettyView) fyne.CanvasObject

NewWrapToggle returns a wrap-text icon toggle bound to pv: it flips between soft-wrap (WrapWord) and horizontal scroll (WrapNone), and is highlighted (HighImportance) while wrapping is on so the state is visible.

func ShowOpenDialog

func ShowOpenDialog(pv *PrettyView, win fyne.Window)

ShowOpenDialog opens Fyne's built-in file-open dialog (an in-canvas widget, not the OS-native picker — Fyne draws its own UI) and loads the chosen file into pv, auto-detecting the format. Exposed so hosts can trigger it from their own menu/button. A host that wants the platform-native dialog should instead set ToolbarConfig.OnOpen (or call its own picker) and feed the bytes to pv.SetData.

Types

type Format

type Format = model.Format

Format selects (or, with FormatAuto, detects) the input grammar. It is an alias of the model package's type so the document model and the public API share one type while keeping the model internal.

type Match

type Match struct {
	Line     int
	ColStart int
	ColEnd   int
}

Match is one search hit, in model coordinates: a stable display-line index and a rune column range [ColStart,ColEnd) into that line's expanded text. All three are int. Keying by line (not visible row) makes a match survive folding; the visible row is an O(log n) lookup. Retrieve the current hits with PrettyView.Matches.

type Option

type Option func(*config)

Option customizes a PrettyView at construction time.

func WithDefaultCollapseDepth

func WithDefaultCollapseDepth(d int) Option

WithDefaultCollapseDepth auto-collapses every container at nesting depth d or deeper on load. Top-level containers are at depth 0, so d=1 collapses everything below the root and d=0 (or any d <= 0) disables auto-collapse.

func WithFormat

func WithFormat(f Format) Option

WithFormat forces a specific input format, skipping auto-detection.

func WithIndentStep

func WithIndentStep(px float32) Option

WithIndentStep sets the pixels of indentation per nesting level.

func WithLineNumbers

func WithLineNumbers() Option

WithLineNumbers renders a line-number gutter to the left of the content. Numbers are the logical display-line indices (1-based) drawn from the struct-of-arrays model, so no extra widgets are created per line and the virtualization invariant holds; wrap-continuation rows leave the gutter blank. Off by default.

func WithMaxInputBytes

func WithMaxInputBytes(n int) Option

WithMaxInputBytes caps the source size SetData/SetText will load. Input longer than n bytes is truncated to n before parsing (the parsers are tolerant, so a truncated document still renders). It bounds the synchronous parse work and the resulting model size (≈5–7× the source, built on the calling/Fyne goroutine) for untrusted or unbounded input. n <= 0 (the default) imposes no cap beyond the model's 4 GiB source ceiling.

func WithSearchConfig

func WithSearchConfig(s SearchConfig) Option

WithSearchConfig overrides the search tuning parameters, merging field-by-field: each NON-ZERO field of s replaces the default, and a zero field keeps its default (MaxMatches 10_000, DebounceFor 150ms, MinQueryLen 1). So setting only MaxMatches leaves debouncing at its default — there is no zero-value trap. To DISABLE keystroke debouncing, set a negative DebounceFor (e.g. -1), or drive input through Search, which always scans immediately (SearchDebounced is the coalescing path).

func WithSyntaxColors

func WithSyntaxColors(v fyne.ThemeVariant, s SyntaxColors) Option

WithSyntaxColors overrides just the syntax token colors for a theme variant (shorthand for WithTheme with only the token fields set).

func WithTabWidth

func WithTabWidth(n int) Option

WithTabWidth sets the display width of a tab character (default 4).

func WithTheme

func WithTheme(v fyne.ThemeVariant, t Theme) Option

WithTheme overrides any of the viewer's colors for a theme variant. Nil fields keep their (Fyne-theme-tracking) defaults; calls compose. Pass the variant your app uses (theme.VariantDark / theme.VariantLight), or set both.

func WithWrap

func WithWrap(m WrapMode) Option

WithWrap selects the long-line handling mode (default WrapNone): WrapNone lets long lines overflow and scroll horizontally (matching Bruno), WrapWord soft-wraps them to the viewport width. The mode can also be changed at runtime with SetWrap.

type PrettyView

type PrettyView struct {
	widget.BaseWidget
	// contains filtered or unexported fields
}

PrettyView is the virtualized structured-data viewer widget.

All state is unexported. Construct one with New or NewWithData and feed it content with SetData / SetText.

PrettyView is not safe for concurrent use: call its methods on the Fyne goroutine (see the package doc's Threading section).

func New

func New(opts ...Option) *PrettyView

New constructs an empty PrettyView, applying zero or more Options.

Example

ExampleNew builds a viewer, loads JSON with auto-detection, adds the optional toolbar, and shows it.

package main

import (
	"fyne.io/fyne/v2/app"
	"fyne.io/fyne/v2/container"

	prettyview "github.com/ideaconnect/go-fyne-pretty-view"
)

func main() {
	a := app.New()
	w := a.NewWindow("viewer")

	pv := prettyview.New(prettyview.WithWrap(prettyview.WrapWord))
	pv.SetData([]byte(`{"hello":"world","items":[1,2,3]}`), prettyview.FormatAuto)

	bar := prettyview.NewToolbar(pv, prettyview.ToolbarConfig{
		ShowSearch:         true,
		ShowExpandCollapse: true,
		ShowWrap:           true,
	})
	w.SetContent(container.NewBorder(bar, nil, nil, nil, pv))
	w.ShowAndRun()
}

func NewWithData

func NewWithData(src []byte, format Format, opts ...Option) *PrettyView

NewWithData constructs a PrettyView and immediately parses src under format.

func (*PrettyView) ClearSearch

func (pv *PrettyView) ClearSearch()

ClearSearch clears search state and highlights. It also cancels any pending debounced scan so a stale query can't repopulate matches after a clear (this is the path SetData uses, so loading new data drops the old document's pending search too).

func (*PrettyView) ClearSelection

func (pv *PrettyView) ClearSelection()

ClearSelection drops any selection.

func (*PrettyView) CollapseAll

func (pv *PrettyView) CollapseAll()

CollapseAll collapses every node below the top level.

func (*PrettyView) CollapseToDepth

func (pv *PrettyView) CollapseToDepth(depth int)

CollapseToDepth collapses every container at nesting depth >= depth (top level is depth 0), leaving shallower containers as they are, and refreshes. ExpandToDepth expands every container at depth < depth. The two compose — e.g. ExpandToDepth(d) then CollapseToDepth(d) shows the tree exactly down to depth d. Call on the Fyne goroutine.

func (*PrettyView) CopySelection

func (pv *PrettyView) CopySelection()

CopySelection copies SelectedText to the clipboard (no-op if empty).

func (*PrettyView) CopySubtree

func (pv *PrettyView) CopySubtree(byteOffset int) bool

CopySubtree copies the displayed text of the node owning byteOffset (its whole {…}/[…]/<tag>…</tag> span, regardless of fold state) to the clipboard, reporting whether a node was found and copied. Source byte offsets are populated for every structured format (JSON/JSONC/XML/HTML); an out-of-range offset returns false. The copied text is the viewer's pretty-printed rendering of the subtree, not the original bytes. (The right-click "Copy subtree" menu item does the same without an offset, for any format.)

Example

ExamplePrettyView_CopySubtree copies a node's whole subtree to the clipboard by its source byte offset (JSON/JSONC; XML/HTML carry no per-node offset and return false). The right-click "Copy subtree" menu item does the same for any format.

package main

import (
	prettyview "github.com/ideaconnect/go-fyne-pretty-view"
)

func main() {
	pv := prettyview.NewWithData([]byte(`{"user":{"name":"ada","id":1}}`), prettyview.FormatJSON)

	// Byte offset of the '{' opening the "user" object in the source.
	pv.CopySubtree(8) // -> true; the "user" subtree is now on the clipboard
}

func (*PrettyView) CreateRenderer

func (pv *PrettyView) CreateRenderer() fyne.WidgetRenderer

CreateRenderer implements fyne.Widget. It builds the scroll + layered content and wires scrolling to the visible-window reflow.

func (*PrettyView) Cursor

func (pv *PrettyView) Cursor() desktop.Cursor

Cursor reports the pointer shape: a pointer over a fold triangle, the text I-beam elsewhere. The over-triangle flag is updated by hover tracking.

func (*PrettyView) DragEnd

func (pv *PrettyView) DragEnd()

func (*PrettyView) Dragged

func (pv *PrettyView) Dragged(ev *fyne.DragEvent)

func (*PrettyView) ExpandAll

func (pv *PrettyView) ExpandAll()

ExpandAll expands every node.

func (*PrettyView) ExpandTo

func (pv *PrettyView) ExpandTo(off int) bool

ExpandTo expands every collapsed ancestor of the node owning byte offset off and scrolls it into view, reporting whether such a node was found. Source byte offsets are populated for every structured format (JSON/JSONC/XML/HTML), so this resolves a node on each; an out-of-range offset returns false without scrolling. See ScrollToLine for a line-index alternative.

func (*PrettyView) ExpandToDepth

func (pv *PrettyView) ExpandToDepth(depth int)

ExpandToDepth expands every container at nesting depth < depth (see CollapseToDepth).

func (*PrettyView) FocusGained

func (pv *PrettyView) FocusGained()

func (*PrettyView) FocusLost

func (pv *PrettyView) FocusLost()

func (*PrettyView) Format

func (pv *PrettyView) Format() Format

Format reports the format actually used for the current document.

func (*PrettyView) KeyDown

func (pv *PrettyView) KeyDown(key *fyne.KeyEvent)

TypedKey handles Escape (clear selection) and keyboard scrolling/navigation: Up/Down scroll one row, PageUp/PageDown one viewport, Home/End jump to the top/ bottom. A multi-megabyte viewer should be navigable without the mouse. KeyDown / KeyUp track the Shift modifier (fyne.KeyEvent carries none), so TypedKey can tell Shift+arrow (extend the keyboard selection) from a plain arrow (scroll).

func (*PrettyView) KeyUp

func (pv *PrettyView) KeyUp(key *fyne.KeyEvent)

func (*PrettyView) Matches

func (pv *PrettyView) Matches() []Match

Matches returns a snapshot of the current search hits in document order (nil if there is no active search), letting a host build its own match list or minimap. The returned slice is a copy — mutating it does not affect the viewer, and it is not updated as the document or search changes; call Matches again after a Search.

func (*PrettyView) MouseDown

func (pv *PrettyView) MouseDown(ev *desktop.MouseEvent)

func (*PrettyView) MouseIn

func (pv *PrettyView) MouseIn(*desktop.MouseEvent)

func (*PrettyView) MouseMoved

func (pv *PrettyView) MouseMoved(ev *desktop.MouseEvent)

func (*PrettyView) MouseOut

func (pv *PrettyView) MouseOut()

func (*PrettyView) MouseUp

func (pv *PrettyView) MouseUp(*desktop.MouseEvent)

func (*PrettyView) Reparse

func (pv *PrettyView) Reparse(format Format)

Reparse re-parses the current source under a different format (e.g. when a UI lets the user override auto-detection). No-op if no document is loaded.

func (*PrettyView) ScrollOffset

func (pv *PrettyView) ScrollOffset() fyne.Position

ScrollOffset returns the current content-space scroll offset (the viewport's top-left). Pair it with SetScrollOffset to save and restore scroll position across a reload or a layout change. Reports the zero position before the widget is shown.

func (*PrettyView) ScrollToLine

func (pv *PrettyView) ScrollToLine(line int) bool

ScrollToLine reveals the given display line (expanding any collapsed ancestors) and scrolls it to the center of the viewport, reporting whether the index is in range. Unlike ExpandTo it takes a display-line index in [0, TotalLines), so it works for every format. Call it on the Fyne goroutine.

func (*PrettyView) Search

func (pv *PrettyView) Search(q SearchQuery)

Search starts or replaces the active search and reveals the first match. It is the IMMEDIATE path: it scans synchronously and returns once matches are computed, and it never debounces. For per-keystroke input from a host search field, prefer SearchDebounced (which coalesces a burst into one scan); use Search when you want the scan to happen now. q.Mode selects SearchPlain or SearchRegex. Call it on the Fyne goroutine.

Example

ExamplePrettyView_Search runs a regular-expression search, reads the live match counter, and steps to the next match.

package main

import (
	prettyview "github.com/ideaconnect/go-fyne-pretty-view"
)

func main() {
	pv := prettyview.NewWithData([]byte(`{"a":"x1","b":"x2","c":"yy"}`), prettyview.FormatJSON)

	pv.Search(prettyview.SearchQuery{Text: `x\d`, Mode: prettyview.SearchRegex})
	pv.SearchStatus() // -> (active, total, capped); total is 2 here
	pv.SearchNext()   // advance to the next match (wraps at the end)
}

func (*PrettyView) SearchDebounced

func (pv *PrettyView) SearchDebounced(q SearchQuery)

SearchDebounced runs Search(q) after the configured SearchConfig.DebounceFor delay, coalescing a burst of rapid calls (e.g. one per keystroke from a host's own search field) so only the last query in the burst scans. With DebounceFor <= 0 it is equivalent to Search (immediate); since WithSearchConfig treats a zero DebounceFor as "keep the 150ms default", pass a NEGATIVE DebounceFor (or call Search) to disable debouncing. Last call wins: a newer SearchDebounced / Search / ClearSearch / SetData supersedes a still-pending scan. Call it on the Fyne goroutine.

func (*PrettyView) SearchError

func (pv *PrettyView) SearchError() error

SearchError reports the error from the most recent Search / SearchDebounced, or nil. The only error is an invalid regular expression (a SearchRegex query whose pattern does not compile); it lets a caller distinguish "the pattern is bad" from "the pattern is valid but matched nothing". It is cleared by the next Search/ClearSearch.

func (*PrettyView) SearchNext

func (pv *PrettyView) SearchNext()

SearchNext moves to the next match (wrapping) and reveals it.

func (*PrettyView) SearchPrev

func (pv *PrettyView) SearchPrev()

SearchPrev moves to the previous match (wrapping) and reveals it.

func (*PrettyView) SearchStatus

func (pv *PrettyView) SearchStatus() (active, total int, capped bool)

SearchStatus returns the active 1-based index, the total match count, and whether the count was capped.

func (*PrettyView) SelectAll

func (pv *PrettyView) SelectAll()

SelectAll selects the entire (currently visible) document.

func (*PrettyView) SelectedText

func (pv *PrettyView) SelectedText() string

SelectedText returns the exact text currently selected, or "".

func (*PrettyView) SetData

func (pv *PrettyView) SetData(src []byte, format Format)

SetData parses src under format (FormatAuto detects) and refreshes the view.

Parsing is SYNCHRONOUS on the calling goroutine (the Fyne goroutine), and builds a compact model ≈5–7× the source size. That is fast for multi-megabyte input but is still O(source) work done before this call returns; to load a large document without a UI hitch, parse off-thread is not supported, so either keep inputs bounded (see WithMaxInputBytes) or accept a brief synchronous cost. Input longer than WithMaxInputBytes (if set) is truncated before parsing.

The src slice is retained: the model holds zero-copy byte ranges into it, so callers must not mutate src after this call (copy it first if it may change).

func (*PrettyView) SetDefaultCollapseDepth

func (pv *PrettyView) SetDefaultCollapseDepth(depth int)

SetDefaultCollapseDepth sets the auto-collapse depth applied on subsequent SetData calls (0 disables).

func (*PrettyView) SetOnDataChanged

func (pv *PrettyView) SetOnDataChanged(fn func())

SetOnDataChanged registers a callback invoked whenever the document is replaced (SetData/SetText/Reparse). Use it to keep host controls (such as a format selector) in sync. Setting it replaces any previous callback.

func (*PrettyView) SetOnSearchChanged

func (pv *PrettyView) SetOnSearchChanged(fn func())

SetOnSearchChanged registers a callback invoked whenever the search match set or active match changes. Use it to keep a host match counter in sync. Setting it replaces any previous callback.

func (*PrettyView) SetOnSearchRequested

func (pv *PrettyView) SetOnSearchRequested(fn func())

SetOnSearchRequested registers a callback invoked when the user presses the search shortcut (Ctrl/Cmd+F), so a host can focus its search field.

func (*PrettyView) SetScrollOffset

func (pv *PrettyView) SetScrollOffset(p fyne.Position)

SetScrollOffset scrolls so p (content space) is the viewport's top-left, clamped to the valid range — e.g. to restore a previously saved ScrollOffset. No-op before the widget is shown. Call it on the Fyne goroutine.

func (*PrettyView) SetSyntaxColors

func (pv *PrettyView) SetSyntaxColors(variant fyne.ThemeVariant, c SyntaxColors)

SetSyntaxColors overrides just the syntax token colors for a theme variant and refreshes (shorthand for SetTheme with only the token fields set).

func (*PrettyView) SetText

func (pv *PrettyView) SetText(s string)

SetText is shorthand for SetData([]byte(s), FormatAuto).

func (*PrettyView) SetTheme

func (pv *PrettyView) SetTheme(variant fyne.ThemeVariant, t Theme)

SetTheme overrides any of the viewer's colors for a theme variant and refreshes. Nil fields keep their defaults; calls compose with earlier WithTheme/WithSyntaxColors/SetTheme overrides for that variant.

func (*PrettyView) SetWrap

func (pv *PrettyView) SetWrap(mode WrapMode)

SetWrap switches long-line handling between WrapNone (horizontal scroll) and WrapWord (soft-wrap to the viewport width) and refreshes. Wrapping is purely presentational: the model, selection, search, and copy are unchanged — a wrapped line still copies as one logical line.

func (*PrettyView) Source

func (pv *PrettyView) Source() []byte

Source returns the bytes of the current document (the originally supplied input), or nil. The returned slice ALIASES the document's retained input buffer; treat it as read-only and copy before mutating, or you corrupt the model the viewer renders from.

func (*PrettyView) Tapped

func (pv *PrettyView) Tapped(e *fyne.PointEvent)

Tapped toggles a fold when the tap lands on a fold triangle. Other taps are left to the selection layer (M8).

func (*PrettyView) TappedSecondary

func (pv *PrettyView) TappedSecondary(e *fyne.PointEvent)

TappedSecondary shows the context menu at the click. This is the standard Fyne pop-up menu — the same themed overlay Entry and read-only selectable text use for their right-click menus (Fyne draws its own UI on the GL canvas, so there is no native OS menu to invoke). A right-click never disturbs the selection (MouseDown returns early for the secondary button), so "Copy" acts on whatever the user had already highlighted.

func (*PrettyView) TypedKey

func (pv *PrettyView) TypedKey(ev *fyne.KeyEvent)

func (*PrettyView) TypedRune

func (pv *PrettyView) TypedRune(rune)

func (*PrettyView) TypedShortcut

func (pv *PrettyView) TypedShortcut(s fyne.Shortcut)

func (*PrettyView) Wrap

func (pv *PrettyView) Wrap() WrapMode

Wrap reports the current long-line handling mode.

type SearchConfig

type SearchConfig struct {
	MaxMatches  int           // cap on stored matches (default 10_000)
	DebounceFor time.Duration // keystroke debounce (default 150ms)
	MinQueryLen int           // shortest query that triggers a scan (default 1)
}

SearchConfig tunes the incremental search behavior. The scan is synchronous on the Fyne goroutine (debounced, and bounded by MaxMatches); see runSearch.

type SearchMode

type SearchMode uint8

SearchMode selects plain-substring or regular-expression matching.

const (
	// SearchPlain matches the query as a literal substring (the default).
	SearchPlain SearchMode = iota
	// SearchRegex matches the query as a Go regular expression (regexp/RE2). RE2
	// matches in time linear in the input, so a host-supplied pattern cannot cause
	// catastrophic backtracking (no ReDoS); an invalid pattern is reported via
	// SearchError. A pathological pattern can still be expensive to compile, so treat
	// the pattern as you would any host input.
	SearchRegex
)

type SearchQuery

type SearchQuery struct {
	Text          string
	Mode          SearchMode
	CaseSensitive bool
}

SearchQuery describes an incremental search request.

type SyntaxColors

type SyntaxColors struct {
	Key     color.Color
	String  color.Color
	Number  color.Color
	Bool    color.Color
	Null    color.Color
	Punct   color.Color
	Tag     color.Color
	Attr    color.Color
	Comment color.Color
}

SyntaxColors is the token-color subset of Theme, kept for the common case of recoloring only the syntax. A nil field keeps the default.

type Theme

type Theme struct {
	// Syntax token colors.
	Key     color.Color
	String  color.Color
	Number  color.Color
	Bool    color.Color
	Null    color.Color
	Punct   color.Color
	Tag     color.Color
	Attr    color.Color
	Comment color.Color

	// Structural / UI colors.
	Foreground  color.Color // default ("plain") text and punctuation fallback
	Summary     color.Color // fold-summary text, e.g. "{ 6 items }"
	IndentGuide color.Color // the vertical indent guide lines
	Selection   color.Color // free-text selection highlight fill
	Match       color.Color // inactive search-match highlight fill
	ActiveMatch color.Color // active search-match highlight fill
}

Theme is the full set of colors the viewer draws. Any nil field falls back to the built-in default for the active theme variant — and the structural defaults (Foreground, Summary, IndentGuide, Selection) themselves follow the host Fyne app theme, so an un-themed PrettyView blends into its surroundings.

Override per variant with WithTheme (construction) or SetTheme (at runtime). Overrides compose: setting only a few fields leaves the rest at their default, and repeated calls / WithSyntaxColors merge rather than replace.

type ToolbarConfig

type ToolbarConfig struct {
	ShowOpen           bool        // an "Open…" button (needs Window or OnOpen)
	ShowFormat         bool        // a format selector (auto/json/jsonc/xml/html/raw)
	ShowExpandCollapse bool        // Expand all / Collapse all buttons
	ShowWrap           bool        // a wrap-text icon toggle (soft-wrap on/off)
	ShowSearch         bool        // a find box with prev/next and a match counter
	Window             fyne.Window // enables the built-in Open dialog and Ctrl/Cmd+F focus
	OnOpen             func()      // overrides the built-in Open behavior, if set
}

ToolbarConfig selects which built-in controls NewToolbar includes. Each is optional; leave a field false to omit that control and provide your own.

func DefaultToolbarConfig

func DefaultToolbarConfig(win fyne.Window) ToolbarConfig

DefaultToolbarConfig enables every control bound to win: the Open button (its built-in file dialog and Ctrl/Cmd+F search-focus both need a Window). Pass nil to omit those two (or set OnOpen yourself afterward to drive Open without a Window).

type WrapMode

type WrapMode uint8

WrapMode controls long-line handling. WrapNone lets long lines overflow and be reached by horizontal scrolling (matching Bruno); WrapWord soft-wraps them to the viewport width (breaking at word boundaries, with a char-break fallback for an unbreakable run). Wrapping is presentational only — selection, search, and copy still operate on whole logical lines.

const (
	WrapNone WrapMode = iota // long lines overflow; horizontal scroll (default)
	WrapWord                 // soft-wrap to the viewport width
)

Directories

Path Synopsis
cmd
prettyview-demo command
Command prettyview-demo exercises the prettyview widget.
Command prettyview-demo exercises the prettyview widget.
Package fonttheme bundles the typefaces go-fyne-pretty-view ships with — JetBrains Mono for monospace (the viewer body) and Inter for UI text — and exposes them as a fyne.Theme you can install on your app.
Package fonttheme bundles the typefaces go-fyne-pretty-view ships with — JetBrains Mono for monospace (the viewer body) and Inter for UI text — and exposes them as a fyne.Theme you can install on your app.
internal
geometry
Package geometry holds the integer-rounded layout math that maps between model positions (line, rune column) and content-space pixels.
Package geometry holds the integer-rounded layout math that maps between model positions (line, rune column) and content-space pixels.
parse
Package parse turns raw bytes into a model.Document.
Package parse turns raw bytes into a model.Document.

Jump to

Keyboard shortcuts

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