Skip to content

Behaviour by format

The renderer's Format is fixed at construction and every method consults it. This page states, per format, exactly what each method does — including the cases where a method does nothing at all, and the options that are silently ignored.

For the symbol list see formats & options; for the errors these calls can return see errors & failure modes.

Which methods produce output in which format

Method text json yaml csv tsv markdown
Write calls textFunc marshals data marshals data tabulates tabulates tabulates
Table aligned table marshals rows marshals rows delimited delimited GFM table
Render styled ANSI no-op styled ANSI styled ANSI styled ANSI styled ANSI
Emit / EmitError no-op envelope no-op no-op no-op no-op
Status / Progress / Spin writes writes writes writes writes writes

Two rows in that table surprise people, so they are stated plainly:

  • Emit is JSON-only. There is no YAML envelope. In every other format Emit and EmitError return nil having written nothing, which is what lets you call EmitError unconditionally on the error path and still fall through to your normal text error handling.
  • Render is a no-op only in JSON. In yaml, csv, tsv and markdown it writes glamour-styled ANSI escape sequences into the stream, which will corrupt output a machine is parsing. Guard it yourself if your tool can be asked for those formats.

The progress family ignores the format entirely — it is driven by interactivity, not by Format. Point it at os.Stderr so it cannot contaminate a machine-readable stdout.

An unrecognised Format value

Format is a string type, so a caller can construct one that is not in Formats(). Both dispatchers fall through to text: Write calls textFunc and Table renders an aligned text table. Nothing errors.

Values arriving from a flag or a config file should go through ParseFormat, whose second return value is the only signal that the input was not understood.

Which table options apply to which format

TableOption values are accepted by every format but honoured by only some:

Option text markdown csv / tsv json / yaml
WithColumns yes yes yes ignored
WithSortBy yes yes yes ignored
WithSortDescending yes yes yes ignored
WithNoHeader yes yes ignored ignored
WithNoTruncation yes n/a — never truncates n/a ignored
WithMaxWidth yes ignored n/a ignored

json and yaml do not go through the column machinery at all — they marshal the row slice as given. Three consequences:

  • Field names and casing come from the json: / yaml: struct tags, not from the table: tags. A column set with WithColumns does not narrow the output.
  • Fields excluded with table:"-" still appear.
  • A sort column that is misspelled or not Sortable returns an error in every other format, but is silently accepted here, because WithSortBy is never consulted.

WithNoHeader is honoured by the text and Markdown renderers only. CSV and TSV always write a header row.

Text table layout

  • Columns are separated by three spaces. Every cell is left-aligned; there is no alignment option.
  • A column's width is Column.Width when non-zero, otherwise the display width of the widest of its header and cells.
  • Trailing spaces are trimmed from each line, so the final column is never padded.
  • Widths are measured with charmbracelet/x/ansi, which counts display columns rather than bytes, so CJK, emoji, combining characters and embedded ANSI do not skew alignment.
Name   Size
a      1

Text table truncation

Unless WithNoTruncation is set, the total table width is compared against the terminal width and columns are shrunk to fit.

  • The width used is WithMaxWidth if given, otherwise term.GetSize(os.Stdout.Fd()), otherwise 80. It is measured against the process's stdout, not the renderer's writer — a renderer pointed at a buffer or a file still lays out at 80 columns.
  • Columns are shrunk right to left, and each may be reduced to a minimum of one column. So the rightmost columns lose their width first, and a narrow terminal can leave a header cut to a single character:
Name               S
averylongname...   1
  • A cell shortened below its content gains a ... tail, unless the remaining width is 3 or less, in which case it is cut without one.
  • Truncation is rune- and ANSI-aware: it never splits a multi-byte rune or an escape sequence in half.

Column.Width is a fixed width, not a minimum or a maximum — a header longer than the width is truncated to it too.

CSV and TSV

Both are written by encoding/csv; TSV is the same writer with the comma replaced by a tab. That means RFC 4180 quoting applies to both: a field containing the delimiter, a double quote, a carriage return or a newline is wrapped in double quotes.

Name,Size
"a,b",1
Name    Size
"a  b"  1

If you need unquoted tab-separated fields, strip the delimiter from your values before rendering — there is no option to disable quoting.

Neither format truncates, and neither honours WithNoHeader.

Markdown

Output is a GitHub-flavoured pipe table with a header separator row, padded so the source lines up in a text editor.

Cell content is escaped before widths are computed, so what is measured is what is written:

  • a literal | becomes \|, so it cannot open a new column;
  • \r\n, \r and \n each become <br>, so a multi-line value cannot open a new row.

Markdown tables are never truncated to terminal width — WithMaxWidth and WithNoTruncation have no effect. A wide table produces a wide line, which is usually what you want when the output is going into a file or a merge request description.

JSON

Write, Table and Emit all use an indented encoder (two spaces) with a trailing newline.

Successive Emit calls append successive documents; they are not collected into an array, and the result is not a single valid JSON document:

{
  "status": "success",
  "command": "a"
}
{
  "status": "success",
  "command": "b"
}

Emit once per command invocation, with everything the caller needs inside Data.

YAML

Write and Table encode via gopkg.in/yaml.v3 with its default settings: no --- document marker, and keys lower-cased from field names unless a yaml: tag says otherwise.

- name: a
  size: 1

There is no YAML equivalent of the JSON envelope. If a script needs status and command alongside the payload in YAML, build a struct carrying those fields and pass it to Write.

A value YAML cannot represent — a channel, a function — makes the encoder panic rather than return an error. Marshal-safe types only.