laslig

package module
v0.2.4 Latest Latest
Warning

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

Go to latest
Published: Apr 2, 2026 License: Apache-2.0 Imports: 13 Imported by: 0

README

Läslig

laslig helps Go CLI tools render attractive, structured terminal output with readable defaults and Go-idiomatic ergonomics.

The package and module name stay laslig. The product branding is Läslig, from the Swedish läslig, meaning legible, and is pronounced roughly LEH-slig.

Visual Examples

Every guided demo item now has its own runnable example under examples/ and its own focused VHS capture under docs/vhs/. mage demo now prints those focused examples one after another as one accumulating walkthrough, with the default braille spinner bridging each example transition. The hero GIF below is a direct capture of that real mage demo flow, while the smaller GIFs underneath stay focused one primitive at a time.

Every runnable example directory under examples/ also carries its own focused README.md with the matching GIF, run commands, and a real library-usage snippet.

The hero demo is intentionally slowed down between sections for README display. Läslig itself does not add runtime delays to your commands by default.

Läslig full demo walkthrough

Run mage demo for the paced aggregate walkthrough in a real terminal, or run any focused example directly with go run ./examples/<name> --format human --style always.

Structured Primitives
Section Notice Record
Section example Notice example Record example
examples/section examples/notice examples/record
KV example List example Table example
examples/kv examples/list examples/table
Panel
Panel example
examples/panel
Rich Text And Progress
Paragraph StatusLine Spinner
Paragraph example StatusLine example Spinner example
examples/paragraph examples/statusline examples/spinner
Markdown CodeBlock LogBlock
Markdown example CodeBlock example LogBlock example
examples/markdown examples/codeblock examples/logblock
Specialized Packages
gotestout Mage-style integration
gotestout example Mage-style integration example
examples/gotestout examples/magecheck

The focused gotestout example intentionally uses a mixed pass/skip/fail fixture so the README shows Läslig's failure rendering too. The separate Mage-style integration example shows the passing task-runner path.

Why

Charm already gives Go developers strong building blocks:

  • Lip Gloss for styling and layout
  • Fang for help, usage, and CLI error presentation

What is still missing is a narrow, reusable layer for ordinary command output: results, notices, summaries, tables, warnings, and errors that should look intentional without forcing an application into a framework.

laslig is that layer.

Status

Today the package includes:

  • output policy and mode resolution
  • document-layout defaults with caller-tunable spacing, section indentation, and list markers
  • a Printer
  • sections
  • notices for info, success, warning, and error output
  • records and lists
  • aligned key-value blocks
  • paragraph blocks
  • compact status lines
  • opt-in transient spinners with stable plain and JSON fallbacks
  • built-in spinner styles: braille, dot, line, pulse, and meter
  • tables
  • panels
  • Glamour-backed Markdown blocks
  • Glamour-backed code blocks
  • boxed log/transcript blocks for caller-provided output
  • compact and detailed gotestout rendering for go test -json
  • caller-tunable gotestout summary and output sections

Deferred for a later release:

  • named theme presets and more ergonomic theme overrides on top of the shipped raw Theme surface
  • deeper gotestout classification and subtest rollups
  • explicit gotestout JSONL capture/export helpers
  • any future standalone Badge or Header primitives if real use cases appear

Principles

  • small, composable helpers instead of a framework
  • writers in, errors out
  • no hidden process control
  • attractive output without requiring Fang or charm/log as core library dependencies
  • easy adoption in Fang, Cobra, Mage, and plain Go commands
  • explicit rendering of caller-provided log excerpts without becoming a logger

Non-Goals

  • replacing application logging
  • replacing command frameworks
  • shipping interactive prompt widgets in v1
  • becoming a kitchen-sink terminal toolkit

Install

go get github.com/evanmschultz/laslig

The current minimum supported Go version is 1.25.8.

Quick Start

package main

import (
	"os"

	"github.com/evanmschultz/laslig"
)

func main() {
	printer := laslig.New(os.Stdout, laslig.Policy{
		Format: laslig.FormatAuto,
		Style:  laslig.StyleAuto,
	})

	_ = printer.Section("release")
	_ = printer.Notice(laslig.Notice{
		Level: laslig.NoticeSuccessLevel,
		Title: "All checks passed",
		Body:  "The CLI can now print structured output with one small helper.",
	})
	_ = printer.Table(laslig.Table{
		Title:  "artifacts",
		Header: []string{"name", "status"},
		Rows: [][]string{
			{"darwin-arm64", "ready"},
			{"linux-amd64", "ready"},
		},
	})
}

Current Surface

printer.Section("Deploy")
printer.Notice(laslig.Notice{Level: laslig.NoticeWarningLevel, Title: "Partial success"})
printer.Record(laslig.Record{Title: "Build"})
printer.KV(laslig.KV{Title: "Config"})
printer.List(laslig.List{Title: "Packages"})
printer.Paragraph(laslig.Paragraph{Title: "Why", Body: "Readable defaults matter."})
printer.StatusLine(laslig.StatusLine{Level: laslig.NoticeSuccessLevel, Text: "Build ready"})
spin := printer.NewSpinner()
_ = spin.Start("Waiting for rollout")
_ = spin.Stop("Rollout ready", laslig.NoticeSuccessLevel)
printer.Table(laslig.Table{Title: "Results"})
printer.Panel(laslig.Panel{Title: "Next step", Body: "Run mage check."})
printer.Markdown(laslig.Markdown{Body: "# Notes\n\n- first\n- second"})
printer.CodeBlock(laslig.CodeBlock{Title: "Example", Language: "go", Body: `fmt.Println("hi")`})
printer.LogBlock(laslig.LogBlock{Title: "stderr excerpt", Body: "INFO boot complete\nWARN retry scheduled"})

Width and wrapping for framed blocks

Tables, panels, code blocks, and log blocks share a bordered width policy in styled human mode:

  • MaxWidth — explicit cap for the rendered block (content + frame), useful for strict layouts.
  • WrapMode — control how long values are compacted when constraints apply.
  • If MaxWidth is omitted, styled blocks shrink toward content width and stay within the available terminal width, with an opinionated readable cap of 88 columns.

Wrap mode is intentionally small and opinionated:

  • TableWrapAuto (default) — wrap words where possible and rebalance columns to stay within the budget.
  • TableWrapTruncate — truncate long values with an ellipsis () to keep one logical line per cell.
  • TableWrapNever — disable wrapping and truncate with an ellipsis when needed.

Today TableWrapNever and TableWrapTruncate render the same way. The library keeps both names because caller intent still matters: never means "do not wrap", while truncate means "compact this by truncating". The duplicate behavior is intentional for now, not an accident.

There is no separate MinWidth knob. The default behavior is deliberately opinionated: fit the content when possible, respect the available terminal width, and cap readable framed output before it becomes too wide.

The focused framed examples also accept --content long, --max-width <n>, and --wrap-mode auto|truncate|never so width behavior can be inspected directly:

go run ./examples/table --format human --style always --content long --max-width 48 --wrap-mode truncate
go run ./examples/panel --format human --style always --content long --max-width 48 --wrap-mode auto
go run ./examples/codeblock --format human --style always --content long --max-width 48 --wrap-mode never
go run ./examples/logblock --format human --style always --content long --max-width 48 --wrap-mode truncate

Terminal-width sweeps are useful when checking the adaptation strategy directly:

COLUMNS=96 go run ./examples/table --format human --style always --content long --wrap-mode auto
COLUMNS=72 go run ./examples/table --format human --style always --content long --wrap-mode auto
COLUMNS=56 go run ./examples/table --format human --style always --content long --wrap-mode auto
COLUMNS=48 go run ./examples/table --format human --style always --content long --wrap-mode truncate

COLUMNS=72 go run ./examples/panel --format human --style always --content long --max-width 48 --wrap-mode auto
COLUMNS=72 go run ./examples/codeblock --format human --style always --content long --max-width 48 --wrap-mode truncate
COLUMNS=72 go run ./examples/logblock --format human --style always --content long --max-width 48 --wrap-mode never
printer.Table(laslig.Table{
	Title:  "Artifacts",
	MaxWidth: 58,
	WrapMode: laslig.TableWrapAuto,
	Header: []string{"artifact_ref", "run_id", "created"},
	Rows: [][]string{
		{
			"github.com/evanmschultz/hylla-fixture-go-2/pkg/very-long-artifact-reference/module",
			"run_2026-04-01T00:00:00.123456789Z_very_long",
			"2026-04-01T00:00:00Z",
		},
	},
})

printer.Panel(laslig.Panel{
	Title:    "Release note",
	MaxWidth: 42,
	WrapMode: laslig.TableWrapTruncate,
	Body:     "Artifacts with long refs and run ids are preserved while still fitting constrained terminals.",
})

FormatAuto resolves to human output on a terminal and plain text otherwise. StyleAuto enables ANSI styling only when the writer is attached to a TTY.

Layout

Läslig now treats output more like a document by default:

  • one leading blank line before the first rendered block
  • one blank line between ordinary blocks
  • stronger separation before new sections
  • section-owned indentation for blocks that follow a Section

Commands can tune that shape when they need something flatter:

layout := laslig.DefaultLayout().
	WithLeadingGap(0).
	WithSectionIndent(0).
	WithListMarker(laslig.ListMarkerBullet)

printer := laslig.New(os.Stdout, laslig.Policy{
	Format: laslig.FormatAuto,
	Style:  laslig.StyleAuto,
	Layout: &layout,
})

Policy can also carry a raw Theme override when one command wants to swap the default styles directly. Higher-level theme presets are still deferred for a later release.

Markdown and code blocks render through Glamour and default to its dracula preset. Commands can override that with Policy.GlamourStyle.

JSON Mode

The same primitives can render machine-readable payloads:

printer := laslig.New(os.Stdout, laslig.Policy{
	Format: laslig.FormatJSON,
})

That makes it practical to keep one semantic output path while exposing human, plain, and JSON surfaces from the same command.

Rich Text, Code, And Logs

laslig can now render wrapped prose, Markdown, code, and caller-provided log excerpts without taking over logging itself:

_ = printer.Paragraph(laslig.Paragraph{
	Title:  "Why",
	Body:   "Readable long-form CLI output should not require a hand-built Lip Gloss layout every time.",
	Footer: "Writers in, errors out.",
})

_ = printer.StatusLine(laslig.StatusLine{
	Level:  laslig.NoticeSuccessLevel,
	Text:   "Build ready",
	Detail: "mage check",
})

spin := printer.NewSpinner()
_ = spin.Start("Waiting for remote rollout")
_ = spin.Stop("Rollout ready", laslig.NoticeSuccessLevel)

_ = printer.Markdown(laslig.Markdown{
	Body: "# Release Notes\n\n## Highlights\n\n- one renderer\n- three output surfaces",
})

_ = printer.CodeBlock(laslig.CodeBlock{
	Title:    "example.go",
	Language: "go",
	Body:     `fmt.Println("hello from laslig")`,
	Footer:   "Rendered through Glamour for terminal output.",
})

_ = printer.LogBlock(laslig.LogBlock{
	Title: "stderr excerpt",
	Body:  "INFO boot complete\nWARN retry scheduled\nERROR dependency missing",
})

Markdown and CodeBlock use Glamour for rich terminal rendering when styled human output is active. LogBlock is for explicit caller-provided excerpts, transcripts, and stderr captures. laslig still does not replace the application's logger.

Use Spinner only when work may otherwise be silent for several seconds. For short operations, a durable StatusLine or Notice is usually clearer. Built-in spinner styles are braille, dot, line, pulse, and meter. The default is braille. Set Policy.SpinnerStyle for a printer-wide default or call printer.NewSpinnerWithStyle(...) for one explicit spinner instance.

Structured Test Output

The gotestout subpackage parses and renders go test -json streams without taking over command execution:

import (
	"errors"
	"os"
	"os/exec"

	"github.com/evanmschultz/laslig"
	"github.com/evanmschultz/laslig/gotestout"
)

cmd := exec.Command("go", "test", "-json", "./...")
stdout, err := cmd.StdoutPipe()
if err != nil {
	return err
}
cmd.Stderr = os.Stderr

if err := cmd.Start(); err != nil {
	return err
}

summary, err := gotestout.Render(os.Stdout, stdout, gotestout.Options{
	Policy: laslig.Policy{
		Format: laslig.FormatAuto,
		Style:  laslig.StyleAuto,
	},
	View: gotestout.ViewCompact,
	DisabledSections: []gotestout.Section{
		gotestout.SectionSkippedTests,
	},
})
if err != nil {
	return err
}

if err := cmd.Wait(); err != nil {
	return err
}
if summary.HasFailures() {
	return errors.New("tests failed")
}

That shape works well in ordinary Go CLIs, Mage targets, Cobra/Fang commands, and small Go helpers invoked from tools like make, just, or task. laslig stays responsible for rendering, while the caller stays responsible for process control. Callers can also disable grouped failed-test, skipped-test, package-error, or captured-output sections when they want a tighter stream.

gotestout also supports one live transient activity block for active test streams. The activity view defaults to auto, which means styled human terminal output gets one transient spinner-led block while the stream is active, while plain, unstyled human, and JSON output stay stable and non-transient. Callers can force that block on with gotestout.ActivityOn for demos or disable it entirely with gotestout.ActivityOff.

This repository dogfoods that pattern in magefiles/magefile.go: mage test runs go test -json ./..., renders compact package and failure output through gotestout, and still returns a normal Mage error on failure. The focused runnable example for that package lives in examples/gotestout/main.go. The separate Mage-facing example in examples/magecheck/main.go shows the same passing task-runner path with gotestout's live activity block enabled while the test stream is active.

Common ways to try that surface locally:

go run ./examples/gotestout --format human --style always
mage test

The focused gotestout GIF and example command intentionally include passing, skipped, and failing test events plus one package build failure. The separate magecheck GIF shows the passing task-runner path with the live activity block active during the running stream. That keeps the README honest about both the success path and the failure path.

Future gotestout work is about smarter summaries, not basic functionality: clearer buckets for test failures vs package/build failures, subtest-aware rollups, and better handling for noisy captured output.

Demo

Focused runnable examples now live one-per-item under examples/: section, notice, record, kv, list, table, panel, paragraph, statusline, spinner, markdown, codeblock, logblock, gotestout, and magecheck. The aggregate walkthrough renderer also lives in examples/all/main.go. The focused logblock example captures real charm.land/log/v2 output internally so the demo still shows an actual Charm log transcript without making charm/log part of laslig's public API. Small verified Go doc examples live in example_test.go. mage demo is the paced walkthrough entrypoint. It renders the focused examples one after another as one accumulating document, using the default braille spinner between examples so the walkthrough never pauses silently. examples/all remains the aggregate renderer for direct example runs and tests.

Run it locally:

mage demo
go run ./examples/section --format human --style always
go run ./examples/notice --format human --style always
go run ./examples/spinner --format human --style always
go run ./examples/gotestout --format human --style always
go run ./examples/magecheck --format human --style always
go run ./examples/all --format human --style always
mage test

mage demo is the normal paced walkthrough entrypoint. mage test is the real Mage-facing gotestout dogfood path. The magecheck focused example demonstrates the live activity block during the running test stream. The go run forms above show the focused per-item examples directly, while go run ./examples/all renders the aggregate example without the paced demo wrapper.

The README GIFs are generated from the focused VHS tapes under docs/vhs/. mage vhs renders all tracked tapes so the README stays aligned with the runnable examples.

Deferred

  • theme presets and higher-level theme configuration
  • richer gotestout failure classification and subtest rollups
  • explicit gotestout JSONL capture/export helpers
  • future standalone Badge or Header primitives only if real use cases appear

Development

This repository uses Mage for local automation.

Install the same Mage version used in CI:

go install github.com/magefile/mage@v1.17.0
mage check
mage test
mage build
mage demo
mage vhs

See CONTRIBUTING.md for contributor workflow details and SECURITY.md for vulnerability reporting guidance.

Structural terminal output is also covered by Charm x/exp/golden snapshots in the shared example renderer, the aggregate demo, the focused gotestout demo, and the gotestout package. Update them intentionally with:

go test ./internal/examples -args -update
go test ./examples/all -args -update
go test ./examples/gotestout -args -update
go test ./gotestout -run 'TestRenderPlainCompactGolden|TestRenderHumanStyledCompactGolden' -args -update

README examples and terminal GIFs are generated from the focused runnable demos under examples/ and the tracked VHS tapes under docs/vhs/.

License

laslig is licensed under Apache-2.0.

Plan

The tracked execution plan lives in PLAN.md.

Documentation

Overview

Package laslig provides helpers for attractive, structured terminal output in Go CLI tools.

Laslig is designed to sit above low-level styling and layout primitives and below command frameworks. It focuses on ordinary command output such as sections, notices, records, KV blocks, lists, tables, panels, paragraphs, status lines, transient spinners, Markdown blocks, code blocks, and log blocks.

The package is intentionally small and data-oriented. Callers provide an io.Writer and a Policy, then render semantic blocks through a Printer. Policy may also carry a Layout when a command wants to tune the default document rhythm, section indentation, or list marker shape, a Theme when one command wants to swap the default styles directly, or a built-in spinner style when one command wants a different transient progress frame set. Laslig does not own logging, command parsing, or process lifecycle. Callers may render explicit log excerpts or transcripts through laslig, but logging policy and sinks remain application concerns.

Policy resolution supports three useful surfaces:

  • human output for terminals
  • plain text for non-terminal writers
  • JSON payloads for machine-readable consumers

A specialist gotestout package provides structured rendering for go test -json streams in Mage targets, ordinary Go CLI commands, and small Go helpers invoked from tools such as make or just, including an optional live spinner footer for styled human terminals while a test stream is active.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type CodeBlock

type CodeBlock struct {
	Title    string `json:"title,omitempty"`
	Language string `json:"language,omitempty"`
	Body     string `json:"body"`
	Footer   string `json:"footer,omitempty"`
	// MaxWidth caps the total frame width (content + frame) in styled human
	// mode. When omitted, the block shrinks toward content width, stays within
	// the available terminal width, and uses Läslig's readable default cap.
	MaxWidth int `json:"maxWidth,omitempty"`
	// WrapMode controls how long block text is compacted when constrained.
	WrapMode TableWrapMode `json:"wrapMode,omitempty"`
}

CodeBlock describes one titled code-style block with optional language hinting.

type Field

type Field struct {
	Label      string `json:"label"`
	Value      string `json:"value"`
	Identifier bool   `json:"identifier,omitempty"`
	Muted      bool   `json:"muted,omitempty"`
	Badge      bool   `json:"badge,omitempty"`
}

Field describes one labeled value in records and list items.

type Format

type Format string

Format identifies the output representation used for a render operation.

const (
	// FormatAuto resolves to a human-oriented format on terminals and plain text otherwise.
	FormatAuto Format = "auto"
	// FormatHuman renders human-oriented structured output.
	FormatHuman Format = "human"
	// FormatPlain renders plain text without terminal styling.
	FormatPlain Format = "plain"
	// FormatJSON renders machine-readable JSON payloads.
	FormatJSON Format = "json"
)

type GlamourStyle

type GlamourStyle string

GlamourStyle identifies one supported built-in Glamour style preset.

Supported built-ins are dark, light, pink, dracula, tokyo-night, ascii, and notty.

const (
	// GlamourStyleDark renders Markdown with Glamour's dark preset.
	GlamourStyleDark GlamourStyle = "dark"
	// GlamourStyleLight renders Markdown with Glamour's light preset.
	GlamourStyleLight GlamourStyle = "light"
	// GlamourStylePink renders Markdown with Glamour's pink preset.
	GlamourStylePink GlamourStyle = "pink"
	// GlamourStyleDracula renders Markdown with Glamour's Dracula preset.
	GlamourStyleDracula GlamourStyle = "dracula"
	// GlamourStyleTokyoNight renders Markdown with Glamour's Tokyo Night preset.
	GlamourStyleTokyoNight GlamourStyle = "tokyo-night"
	// GlamourStyleASCII renders Markdown with Glamour's ASCII preset.
	GlamourStyleASCII GlamourStyle = "ascii"
	// GlamourStyleNoTTY renders Markdown with Glamour's no-TTY preset.
	GlamourStyleNoTTY GlamourStyle = "notty"
)

func DefaultGlamourStyle

func DefaultGlamourStyle() GlamourStyle

DefaultGlamourStyle returns the default built-in Glamour style used by laslig, which is dracula.

func (GlamourStyle) Valid

func (s GlamourStyle) Valid() bool

Valid reports whether the style matches one of laslig's supported built-in Glamour presets.

type KV

type KV struct {
	Title string  `json:"title,omitempty"`
	Pairs []Field `json:"pairs,omitempty"`
	Empty string  `json:"empty,omitempty"`
}

KV describes one aligned key-value block.

type Layout

type Layout struct {
	// contains filtered or unexported fields
}

Layout describes the high-level document rhythm used by one printer.

Use DefaultLayout as a base, then override individual values with the builder-style helpers when a command wants a different shape.

func DefaultLayout

func DefaultLayout() Layout

DefaultLayout returns the opinionated default document layout used by laslig.

func (Layout) WithBlockGap

func (l Layout) WithBlockGap(count int) Layout

WithBlockGap returns one layout with an updated ordinary block gap.

func (Layout) WithLeadingGap

func (l Layout) WithLeadingGap(count int) Layout

WithLeadingGap returns one layout with an updated leading gap.

func (Layout) WithListMarker

func (l Layout) WithListMarker(marker ListMarker) Layout

WithListMarker returns one layout with an updated list marker style.

func (Layout) WithSectionGap

func (l Layout) WithSectionGap(count int) Layout

WithSectionGap returns one layout with an updated section gap.

func (Layout) WithSectionIndent

func (l Layout) WithSectionIndent(count int) Layout

WithSectionIndent returns one layout with an updated section-body indent.

type List

type List struct {
	Title string     `json:"title"`
	Items []ListItem `json:"items,omitempty"`
	Empty string     `json:"empty,omitempty"`
}

List describes one titled list block.

type ListItem

type ListItem struct {
	Title  string  `json:"title"`
	Badge  string  `json:"badge,omitempty"`
	Fields []Field `json:"fields,omitempty"`
}

ListItem describes one item in a rendered list.

type ListMarker

type ListMarker string

ListMarker identifies the marker shape used for unordered and ordered list output.

const (
	// ListMarkerDash renders list items with a dash marker.
	ListMarkerDash ListMarker = "dash"
	// ListMarkerBullet renders list items with a bullet marker.
	ListMarkerBullet ListMarker = "bullet"
	// ListMarkerNumber renders list items with an ordinal marker.
	ListMarkerNumber ListMarker = "number"
)

type LogBlock

type LogBlock struct {
	Title  string `json:"title,omitempty"`
	Body   string `json:"body"`
	Footer string `json:"footer,omitempty"`
	// MaxWidth caps the total frame width (content + frame) in styled human
	// mode. When omitted, the block shrinks toward content width, stays within
	// the available terminal width, and uses Läslig's readable default cap.
	MaxWidth int `json:"maxWidth,omitempty"`
	// WrapMode controls how long block text is compacted when constrained.
	WrapMode TableWrapMode `json:"wrapMode,omitempty"`
}

LogBlock describes one titled boxed transcript or log excerpt.

type Markdown

type Markdown struct {
	Title  string `json:"title,omitempty"`
	Body   string `json:"body"`
	Footer string `json:"footer,omitempty"`
}

Markdown describes one Markdown block rendered for terminal output.

type Mode

type Mode struct {
	Format Format
	Styled bool
	Width  int
}

Mode describes the resolved output behavior for one writer, including the detected terminal width when available.

func ResolveMode

func ResolveMode(out io.Writer, policy Policy) Mode

ResolveMode resolves one writer and policy into a concrete output mode.

type Notice

type Notice struct {
	Level  NoticeLevel `json:"level"`
	Title  string      `json:"title,omitempty"`
	Body   string      `json:"body,omitempty"`
	Detail []string    `json:"detail,omitempty"`
}

Notice describes one user-facing diagnostic block.

type NoticeLevel

type NoticeLevel string

NoticeLevel identifies one user-facing diagnostic level.

const (
	// NoticeInfoLevel identifies informational notices.
	NoticeInfoLevel NoticeLevel = "info"
	// NoticeSuccessLevel identifies success notices.
	NoticeSuccessLevel NoticeLevel = "success"
	// NoticeWarningLevel identifies warning notices.
	NoticeWarningLevel NoticeLevel = "warning"
	// NoticeErrorLevel identifies error notices.
	NoticeErrorLevel NoticeLevel = "error"
)

type Panel

type Panel struct {
	Title  string `json:"title,omitempty"`
	Body   string `json:"body"`
	Footer string `json:"footer,omitempty"`
	// MaxWidth caps the total panel width (content + border/padding) in human
	// mode. When omitted, the panel shrinks toward content width, stays within
	// the available terminal width, and uses Läslig's readable default cap.
	MaxWidth int `json:"maxWidth,omitempty"`
	// WrapMode controls how long panel body/footer lines are compacted.
	WrapMode TableWrapMode `json:"wrapMode,omitempty"`
}

Panel describes one titled boxed block.

type Paragraph

type Paragraph struct {
	Title  string `json:"title,omitempty"`
	Body   string `json:"body"`
	Footer string `json:"footer,omitempty"`
}

Paragraph describes one wrapped long-form text block.

type Policy

type Policy struct {
	// Format selects the overall render format.
	Format Format
	// Style controls ANSI styling for human output.
	Style StylePolicy
	// Layout overrides the default document spacing and indentation rules.
	Layout *Layout
	// Theme overrides the printer-wide lipgloss style roles.
	Theme *Theme
	// SpinnerStyle selects the built-in transient spinner frame set used by
	// Printer.NewSpinner. Supported values are braille, dot, line, pulse, and
	// meter. The default is braille.
	SpinnerStyle SpinnerStyle
	// GlamourStyle selects the built-in Glamour preset used for Markdown and
	// code-block rendering. Supported values are dark, light, pink, dracula,
	// tokyo-night, ascii, and notty. The default is dracula.
	GlamourStyle GlamourStyle
}

Policy describes the requested output behavior before writer capabilities are resolved.

type Printer

type Printer struct {
	// contains filtered or unexported fields
}

Printer renders structured output to one writer.

func New

func New(out io.Writer, policy Policy) *Printer

New constructs one printer by resolving a writer against the provided policy.

func NewWithMode

func NewWithMode(out io.Writer, mode Mode) *Printer

NewWithMode constructs one printer using an already-resolved output mode.

NewWithMode is a convenience for callers that already resolved the output mode and are happy with the default Layout and Theme for that mode.

func (*Printer) CodeBlock

func (p *Printer) CodeBlock(block CodeBlock) error

CodeBlock writes one titled code-style block.

Example

ExamplePrinter_CodeBlock shows one plain code block without ANSI styling.

package main

import (
	"os"

	"github.com/evanmschultz/laslig"
)

// newExamplePrinter constructs one plain printer without the default leading gap
// so package examples stay compact in rendered Go docs.
func newExamplePrinter() *laslig.Printer {
	layout := laslig.DefaultLayout().WithLeadingGap(0)
	return laslig.New(os.Stdout, laslig.Policy{
		Format: laslig.FormatPlain,
		Style:  laslig.StyleNever,
		Layout: &layout,
	})
}

func main() {
	printer := newExamplePrinter()

	_ = printer.CodeBlock(laslig.CodeBlock{
		Title:    "Snippet",
		Language: "go",
		Body:     "fmt.Println(\"hello from laslig\")",
		Footer:   "Use CodeBlock for commands or code samples.",
	})

}
Output:
Snippet

fmt.Println("hello from laslig")

Use CodeBlock for commands or code samples.

func (*Printer) KV

func (p *Printer) KV(kv KV) error

KV writes one aligned key-value block.

Example

ExamplePrinter_KV shows aligned key-value rendering.

package main

import (
	"os"

	"github.com/evanmschultz/laslig"
)

// newExamplePrinter constructs one plain printer without the default leading gap
// so package examples stay compact in rendered Go docs.
func newExamplePrinter() *laslig.Printer {
	layout := laslig.DefaultLayout().WithLeadingGap(0)
	return laslig.New(os.Stdout, laslig.Policy{
		Format: laslig.FormatPlain,
		Style:  laslig.StyleNever,
		Layout: &layout,
	})
}

func main() {
	printer := newExamplePrinter()

	_ = printer.KV(laslig.KV{
		Title: "Config",
		Pairs: []laslig.Field{
			{Label: "module", Value: "github.com/evanmschultz/laslig", Identifier: true},
			{Label: "style", Value: "auto", Badge: true},
			{Label: "runner", Value: "mage", Muted: true},
		},
	})

}
Output:
Config
  module  github.com/evanmschultz/laslig
  style   [AUTO]
  runner  mage

func (*Printer) List

func (p *Printer) List(list List) error

List writes one titled list block.

Example

ExamplePrinter_List shows grouped list items with badges and detail fields.

package main

import (
	"os"

	"github.com/evanmschultz/laslig"
)

// newExamplePrinter constructs one plain printer without the default leading gap
// so package examples stay compact in rendered Go docs.
func newExamplePrinter() *laslig.Printer {
	layout := laslig.DefaultLayout().WithLeadingGap(0)
	return laslig.New(os.Stdout, laslig.Policy{
		Format: laslig.FormatPlain,
		Style:  laslig.StyleNever,
		Layout: &layout,
	})
}

func main() {
	printer := newExamplePrinter()

	_ = printer.List(laslig.List{
		Title: "Targets",
		Items: []laslig.ListItem{
			{
				Title: "check",
				Badge: "ready",
				Fields: []laslig.Field{
					{Label: "when", Value: "Run verification before handoff."},
				},
			},
			{
				Title: "demo",
				Badge: "live",
				Fields: []laslig.Field{
					{Label: "what", Value: "Show the all-in-one walkthrough."},
				},
			},
		},
	})

}
Output:
Targets
- check [READY]
  when: Run verification before handoff.
- demo [LIVE]
  what: Show the all-in-one walkthrough.

func (*Printer) LogBlock

func (p *Printer) LogBlock(block LogBlock) error

LogBlock writes one titled boxed transcript or log excerpt.

Example

ExamplePrinter_LogBlock shows one plain boxed-log surface without owning logging.

package main

import (
	"os"

	"github.com/evanmschultz/laslig"
)

// newExamplePrinter constructs one plain printer without the default leading gap
// so package examples stay compact in rendered Go docs.
func newExamplePrinter() *laslig.Printer {
	layout := laslig.DefaultLayout().WithLeadingGap(0)
	return laslig.New(os.Stdout, laslig.Policy{
		Format: laslig.FormatPlain,
		Style:  laslig.StyleNever,
		Layout: &layout,
	})
}

func main() {
	printer := newExamplePrinter()

	_ = printer.LogBlock(laslig.LogBlock{
		Title: "stderr excerpt",
		Body:  "INFO boot complete\nWARN retry scheduled",
	})

}
Output:
stderr excerpt

INFO boot complete
WARN retry scheduled

func (*Printer) Markdown

func (p *Printer) Markdown(block Markdown) error

Markdown writes one Markdown block.

Example

ExamplePrinter_Markdown shows one Markdown block rendered as source in plain mode.

package main

import (
	"os"

	"github.com/evanmschultz/laslig"
)

// newExamplePrinter constructs one plain printer without the default leading gap
// so package examples stay compact in rendered Go docs.
func newExamplePrinter() *laslig.Printer {
	layout := laslig.DefaultLayout().WithLeadingGap(0)
	return laslig.New(os.Stdout, laslig.Policy{
		Format: laslig.FormatPlain,
		Style:  laslig.StyleNever,
		Layout: &layout,
	})
}

func main() {
	printer := newExamplePrinter()

	_ = printer.Markdown(laslig.Markdown{
		Body: "# Notes\n\n- first\n- second",
	})

}
Output:
# Notes

- first
- second

func (*Printer) Mode

func (p *Printer) Mode() Mode

Mode returns the resolved output mode used by the printer.

func (*Printer) NewSpinner added in v0.2.0

func (p *Printer) NewSpinner() *Spinner

NewSpinner constructs one opt-in transient progress helper bound to the printer's resolved mode, layout, theme, and default spinner style.

Styled human terminals animate one transient line in place. Plain output, human output with StyleNever, and JSON output degrade to stable start/finish status records without transient frames.

Example

ExamplePrinter_NewSpinner shows one spinner falling back to stable status rows in plain output.

package main

import (
	"os"

	"github.com/evanmschultz/laslig"
)

// newExamplePrinter constructs one plain printer without the default leading gap
// so package examples stay compact in rendered Go docs.
func newExamplePrinter() *laslig.Printer {
	layout := laslig.DefaultLayout().WithLeadingGap(0)
	return laslig.New(os.Stdout, laslig.Policy{
		Format: laslig.FormatPlain,
		Style:  laslig.StyleNever,
		Layout: &layout,
	})
}

func main() {
	printer := newExamplePrinter()
	spin := printer.NewSpinner()

	_ = spin.Start("Waiting for rollout")
	_ = spin.Stop("Rollout ready", laslig.NoticeSuccessLevel)

}
Output:
[RUNNING] Waiting for rollout
[SUCCESS] Rollout ready

func (*Printer) NewSpinnerWithStyle added in v0.2.1

func (p *Printer) NewSpinnerWithStyle(style SpinnerStyle) *Spinner

NewSpinnerWithStyle constructs one opt-in transient progress helper bound to the printer, using one specific built-in spinner frame set.

Supported styles are braille, dot, line, pulse, and meter. Invalid values fall back to the default braille style.

Example

ExamplePrinter_NewSpinnerWithStyle shows one spinner using an explicit built-in frame set.

package main

import (
	"os"

	"github.com/evanmschultz/laslig"
)

// newExamplePrinter constructs one plain printer without the default leading gap
// so package examples stay compact in rendered Go docs.
func newExamplePrinter() *laslig.Printer {
	layout := laslig.DefaultLayout().WithLeadingGap(0)
	return laslig.New(os.Stdout, laslig.Policy{
		Format: laslig.FormatPlain,
		Style:  laslig.StyleNever,
		Layout: &layout,
	})
}

func main() {
	printer := newExamplePrinter()
	spin := printer.NewSpinnerWithStyle(laslig.SpinnerStyleLine)

	_ = spin.Start("Waiting for rollout")
	_ = spin.Stop("Rollout ready", laslig.NoticeSuccessLevel)

}
Output:
[RUNNING] Waiting for rollout
[SUCCESS] Rollout ready

func (*Printer) Notice

func (p *Printer) Notice(notice Notice) error

Notice writes one user-facing notice block.

Example

ExamplePrinter_Notice shows one warning notice rendered without ANSI styling.

package main

import (
	"os"

	"github.com/evanmschultz/laslig"
)

// newExamplePrinter constructs one plain printer without the default leading gap
// so package examples stay compact in rendered Go docs.
func newExamplePrinter() *laslig.Printer {
	layout := laslig.DefaultLayout().WithLeadingGap(0)
	return laslig.New(os.Stdout, laslig.Policy{
		Format: laslig.FormatPlain,
		Style:  laslig.StyleNever,
		Layout: &layout,
	})
}

func main() {
	printer := newExamplePrinter()

	_ = printer.Notice(laslig.Notice{
		Level: laslig.NoticeWarningLevel,
		Title: "Coverage dropped",
		Body:  "Package coverage fell below the configured threshold.",
		Detail: []string{
			"Previous: 84.2%",
			"Current:  79.8%",
		},
	})

}
Output:
[WARNING] Coverage dropped
  Package coverage fell below the configured threshold.
  Previous: 84.2%
  Current:  79.8%

func (*Printer) Panel

func (p *Printer) Panel(panel Panel) error

Panel writes one titled panel block.

Example

ExamplePrinter_Panel shows one stronger callout block in plain mode.

package main

import (
	"os"

	"github.com/evanmschultz/laslig"
)

// newExamplePrinter constructs one plain printer without the default leading gap
// so package examples stay compact in rendered Go docs.
func newExamplePrinter() *laslig.Printer {
	layout := laslig.DefaultLayout().WithLeadingGap(0)
	return laslig.New(os.Stdout, laslig.Policy{
		Format: laslig.FormatPlain,
		Style:  laslig.StyleNever,
		Layout: &layout,
	})
}

func main() {
	printer := newExamplePrinter()

	_ = printer.Panel(laslig.Panel{
		Title:  "Next step",
		Body:   "Run mage check before pushing.",
		Footer: "Use Panel when the note should stand apart from the surrounding document.",
	})

}
Output:
Next step

Run mage check before pushing.

Use Panel when the note should stand apart from the surrounding document.

func (*Printer) Paragraph

func (p *Printer) Paragraph(paragraph Paragraph) error

Paragraph writes one wrapped long-form text block.

Example

ExamplePrinter_Paragraph shows one wrapped long-form text block in plain mode.

package main

import (
	"os"

	"github.com/evanmschultz/laslig"
)

// newExamplePrinter constructs one plain printer without the default leading gap
// so package examples stay compact in rendered Go docs.
func newExamplePrinter() *laslig.Printer {
	layout := laslig.DefaultLayout().WithLeadingGap(0)
	return laslig.New(os.Stdout, laslig.Policy{
		Format: laslig.FormatPlain,
		Style:  laslig.StyleNever,
		Layout: &layout,
	})
}

func main() {
	printer := newExamplePrinter()

	_ = printer.Paragraph(laslig.Paragraph{
		Title:  "Why",
		Body:   "Laslig keeps ordinary command output readable without forcing a framework.",
		Footer: "Writers in, errors out.",
	})

}
Output:
Why

Laslig keeps ordinary command output readable without forcing a framework.

Writers in, errors out.

func (*Printer) Record

func (p *Printer) Record(record Record) error

Record writes one titled record block.

Example

ExamplePrinter_Record shows one simple record render with plain output.

package main

import (
	"os"

	"github.com/evanmschultz/laslig"
)

// newExamplePrinter constructs one plain printer without the default leading gap
// so package examples stay compact in rendered Go docs.
func newExamplePrinter() *laslig.Printer {
	layout := laslig.DefaultLayout().WithLeadingGap(0)
	return laslig.New(os.Stdout, laslig.Policy{
		Format: laslig.FormatPlain,
		Style:  laslig.StyleNever,
		Layout: &layout,
	})
}

func main() {
	printer := newExamplePrinter()

	_ = printer.Record(laslig.Record{
		Title: "Build",
		Fields: []laslig.Field{
			{Label: "status", Value: "pass", Badge: true},
			{Label: "runner", Value: "mage", Muted: true},
		},
	})

}
Output:
Build
  status: [PASS]
  runner: mage

func (*Printer) Section

func (p *Printer) Section(title string) error

Section writes one section heading and opens section-owned indentation for following content blocks until the next section heading.

Example

ExamplePrinter_Section shows one section heading owning the indentation of the blocks that follow it.

package main

import (
	"os"

	"github.com/evanmschultz/laslig"
)

// newExamplePrinter constructs one plain printer without the default leading gap
// so package examples stay compact in rendered Go docs.
func newExamplePrinter() *laslig.Printer {
	layout := laslig.DefaultLayout().WithLeadingGap(0)
	return laslig.New(os.Stdout, laslig.Policy{
		Format: laslig.FormatPlain,
		Style:  laslig.StyleNever,
		Layout: &layout,
	})
}

func main() {
	printer := newExamplePrinter()

	_ = printer.Section("Overview")
	_ = printer.Record(laslig.Record{
		Title: "Record",
		Fields: []laslig.Field{
			{Label: "what", Value: "Use Section when later blocks should clearly read as one document group."},
		},
	})

}
Output:
Overview

  Record
    what: Use Section when later blocks should clearly read as one document group.

func (*Printer) StatusLine

func (p *Printer) StatusLine(line StatusLine) error

StatusLine writes one compact semantic single-line status row.

Example

ExamplePrinter_StatusLine shows one compact status row in plain mode.

package main

import (
	"os"

	"github.com/evanmschultz/laslig"
)

// newExamplePrinter constructs one plain printer without the default leading gap
// so package examples stay compact in rendered Go docs.
func newExamplePrinter() *laslig.Printer {
	layout := laslig.DefaultLayout().WithLeadingGap(0)
	return laslig.New(os.Stdout, laslig.Policy{
		Format: laslig.FormatPlain,
		Style:  laslig.StyleNever,
		Layout: &layout,
	})
}

func main() {
	printer := newExamplePrinter()

	_ = printer.StatusLine(laslig.StatusLine{
		Level:  laslig.NoticeSuccessLevel,
		Text:   "Build ready",
		Detail: "mage check",
	})

}
Output:
[SUCCESS] Build ready (mage check)

func (*Printer) Table

func (p *Printer) Table(table Table) error

Table writes one titled table block.

Example

ExamplePrinter_Table shows one plain table render for stable text output.

package main

import (
	"os"

	"github.com/evanmschultz/laslig"
)

// newExamplePrinter constructs one plain printer without the default leading gap
// so package examples stay compact in rendered Go docs.
func newExamplePrinter() *laslig.Printer {
	layout := laslig.DefaultLayout().WithLeadingGap(0)
	return laslig.New(os.Stdout, laslig.Policy{
		Format: laslig.FormatPlain,
		Style:  laslig.StyleNever,
		Layout: &layout,
	})
}

func main() {
	printer := newExamplePrinter()

	_ = printer.Table(laslig.Table{
		Title:  "Targets",
		Header: []string{"name", "status"},
		Rows: [][]string{
			{"check", "ready"},
			{"demo", "ready"},
		},
		Caption: "One policy, three surfaces.",
	})

}
Output:
Targets
name  | status
------+-------
check | ready
demo  | ready
One policy, three surfaces.

type Record

type Record struct {
	Title  string  `json:"title"`
	Fields []Field `json:"fields,omitempty"`
}

Record describes one labeled data block.

type Spinner added in v0.2.0

type Spinner struct {
	// contains filtered or unexported fields
}

Spinner renders one opt-in transient progress line for long-running work.

Use Spinner when a CLI might otherwise be silent for several seconds and a caller wants a lightweight "still running" signal. Prefer StatusLine or Notice when work starts and finishes quickly enough that durable structured output is enough.

Spinner does not own process lifecycle, cancellation, or retries. Callers should stop a spinner before writing other laslig blocks to the same writer.

func (*Spinner) Start added in v0.2.0

func (s *Spinner) Start(text string) error

Start begins rendering one transient progress line or one stable fallback start record when animation is not appropriate for the resolved output mode.

Styled human output animates one in-place line. Plain output, unstyled human output, and JSON output emit one durable start record instead.

func (*Spinner) Stop added in v0.2.0

func (s *Spinner) Stop(message string, level NoticeLevel) error

Stop finalizes one running spinner with one durable status line or JSON status record.

When message is empty, Stop reuses the most recent spinner text. If level is empty, Stop defaults to NoticeSuccessLevel. Stop is safe to call even when a delayed-start caller never started the spinner.

func (*Spinner) Update added in v0.2.0

func (s *Spinner) Update(text string) error

Update replaces the current spinner text while work is still running.

Updates redraw the transient line on styled human terminals. In plain, unstyled human, and JSON modes, Update records the latest text but does not emit additional output. Call Start before Update.

type SpinnerStyle added in v0.2.1

type SpinnerStyle string

SpinnerStyle identifies one supported built-in spinner frame set.

Supported built-ins are braille, dot, line, pulse, and meter.

const (
	// SpinnerStyleBraille renders the default braille-dot spinner.
	SpinnerStyleBraille SpinnerStyle = "braille"
	// SpinnerStyleDot renders a larger dot spinner.
	SpinnerStyleDot SpinnerStyle = "dot"
	// SpinnerStyleLine renders a compact ASCII-friendly line spinner.
	SpinnerStyleLine SpinnerStyle = "line"
	// SpinnerStylePulse renders a pulsing dot spinner.
	SpinnerStylePulse SpinnerStyle = "pulse"
	// SpinnerStyleMeter renders a meter-like spinner.
	SpinnerStyleMeter SpinnerStyle = "meter"
)

func DefaultSpinnerStyle added in v0.2.1

func DefaultSpinnerStyle() SpinnerStyle

DefaultSpinnerStyle returns the default built-in spinner style used by laslig, which is braille.

func (SpinnerStyle) Valid added in v0.2.1

func (s SpinnerStyle) Valid() bool

Valid reports whether the style matches one of laslig's supported built-in spinner styles.

type StatusLine

type StatusLine struct {
	Level  NoticeLevel `json:"level,omitempty"`
	Label  string      `json:"label,omitempty"`
	Text   string      `json:"text"`
	Detail string      `json:"detail,omitempty"`
}

StatusLine describes one compact semantic status row.

type StylePolicy

type StylePolicy string

StylePolicy controls whether ANSI styling is enabled for human output.

const (
	// StyleAuto enables styling only when output is attached to a terminal.
	StyleAuto StylePolicy = "auto"
	// StyleAlways forces styling for human output.
	StyleAlways StylePolicy = "always"
	// StyleNever disables styling.
	StyleNever StylePolicy = "never"
)

type Table

type Table struct {
	Title   string     `json:"title"`
	Header  []string   `json:"header,omitempty"`
	Rows    [][]string `json:"rows,omitempty"`
	Caption string     `json:"caption,omitempty"`
	Empty   string     `json:"empty,omitempty"`
	// MaxWidth clamps styled table width (content + frame) when rendering in
	// human mode. When omitted, the table shrinks toward content width, stays
	// within the available terminal width, and uses Läslig's readable default
	// cap.
	MaxWidth int `json:"maxWidth,omitempty"`
	// WrapMode controls how long table content is compacted in constrained
	// widths.
	WrapMode TableWrapMode `json:"wrapMode,omitempty"`
}

Table describes one titled table block.

type TableWrapMode added in v0.2.4

type TableWrapMode string

TableWrapMode controls how long framed structured text is compacted in constrained widths.

const (
	// TableWrapAuto wraps where possible and rebalances structured content to
	// fit the available width.
	TableWrapAuto TableWrapMode = "auto"
	// TableWrapNever keeps each logical line unwrapped and truncates if needed.
	// Today this matches TableWrapTruncate intentionally; the separate name keeps
	// the API semantics explicit for callers that want to state "do not wrap".
	TableWrapNever TableWrapMode = "never"
	// TableWrapTruncate truncates long values without wrapping. Today this
	// behaves the same as TableWrapNever and differs mainly in caller intent.
	TableWrapTruncate TableWrapMode = "truncate"
)

type Theme

type Theme struct {
	Section       lipgloss.Style
	Label         lipgloss.Style
	Value         lipgloss.Style
	Identifier    lipgloss.Style
	Muted         lipgloss.Style
	Badge         lipgloss.Style
	Panel         lipgloss.Style
	TableHeader   lipgloss.Style
	TableRule     lipgloss.Style
	NoticeInfo    lipgloss.Style
	NoticeSuccess lipgloss.Style
	NoticeWarning lipgloss.Style
	NoticeError   lipgloss.Style
}

Theme contains the styles used by one printer.

func DefaultTheme

func DefaultTheme(mode Mode) Theme

DefaultTheme returns the default theme for one resolved output mode.

Directories

Path Synopsis
examples
all command
codeblock command
gotestout command
kv command
list command
logblock command
magecheck command
markdown command
notice command
panel command
paragraph command
record command
section command
spinner command
statusline command
table command
Package gotestout renders go test -json streams using laslig output primitives and the library's normal terminal styling behavior.
Package gotestout renders go test -json streams using laslig output primitives and the library's normal terminal styling behavior.
internal

Jump to

Keyboard shortcuts

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