TermMosaic v1.0.0

v1.0.0 is the first release that makes a stability promise: the public API freezes here and Semantic Versioning applies in earnest — with one documented exception: widgets/widgettest is excluded from that promise, because it must evolve with the framework. Read the limitations before you rely on anything here.

Guides

Composing with Block

Borders, padding and solving a layout against Block.Interior() so content never has to know a border exists.

Composing with Block

The composition contract: who owns a border, who owns the padding, and what the inner widget is told about the space it has.

The rule

Block is the only thing in the catalog that draws a border or a title. And Block.Interior() is the rectangle your body draws into.

Everything else either composes a Block or calls one. No widget in the catalog contains a glyph table, a corner loop or a title threshold — and that is not a convention, it is the collision ADR 0008 §2 was written to prevent.

The pattern

func (s *screen) Draw(buf *buffer.Buffer) {
    chrome := block.New(s.bounds)
    chrome.SetBorder(buffer.BorderRounded)
    chrome.SetPadding(1)
    chrome.SetTitleString("New project", stTitle)
    chrome.Draw(buf)

    // Solve against the interior, not against s.bounds. This one line is the
    // whole reason Block exists.
    r := chrome.Interior()

    xs := layout.Solve(layout.Horizontal,
        layout.Fill(1),    // the field takes the slack
        layout.Length(12), // the button is fixed
        2,                 // two cells of spacing
        r.W,
    )
    ys := layout.Solve(layout.Vertical,
        layout.Fill(1), layout.Length(1), 1, r.H,
    )

    s.name.SetBounds(layout.Rect(r, layout.Horizontal, xs, 2, 0))
    s.save.SetBounds(layout.Rect(r, layout.Horizontal, xs, 2, 1))
    s.hint.SetBounds(layout.Rect(r, layout.Vertical, ys, 1, 1))

    s.name.Draw(buf)
    s.save.Draw(buf)
    s.hint.Draw(buf)
}

Three things this gets right that hand-rolled chrome does not:

  1. Interior() knows about the border and the padding, so your layout does not. No +2 for a border, no -2 for padding, and no arithmetic that breaks when you change Padding from 1 to 2.
  2. Block.Draw fills the rect in Background first, which satisfies ADR 0007 §1 rule 3 — the repaint-your-rect rule — without you writing a fill loop.
  3. The title is truncated with a marker and cached against the rect, so a narrow terminal is told what happened instead of losing the title silently.

Block is chrome only

A Block has no children. It draws a border, a title and a background, and that is the whole widget. What goes inside it is the application’s business: compose a widget into the interior, or use Split to place several.

Block rendered by cmd/capture through widgettest.Capture: colour at 3 widths (40, 80, 120 columns), and the exact plain-text cell grid at the widest of them.

MinSize() returns 5 × 5 cells. That is the whole widget including its own chrome, and it is the widget's assertion about itself — below it, Draw clips rather than blanks, and what to do about that is the application's decision.

40 columns × 9 rows

╭ Package layout ──────────────────────╮
│                                      │
│                                      │
│                                      │
│                                      │
│                                      │
│                                      │
│                                      │
╰──────────────────────────────────────╯

80 columns × 9 rows

╭ Package layout ──────────────────────────────────────────────────────────────╮
│                                                                              │
│                                                                              │
│                                                                              │
│                                                                              │
│                                                                              │
│                                                                              │
│                                                                              │
╰──────────────────────────────────────────────────────────────────────────────╯

120 columns × 9 rows

╭ Package layout ──────────────────────────────────────────────────────────────────────────────────────────────────────╮
│                                                                                                                      │
│                                                                                                                      │
│                                                                                                                      │
│                                                                                                                      │
│                                                                                                                      │
│                                                                                                                      │
│                                                                                                                      │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯

Plain text — the exact cell grid, trimmed per line. This is the honest form: Braille and block-element glyphs can take a different advance width in a web font than a terminal gives them, which breaks the alignment they depend on.

# Block — A bordered, titled, padded rectangle that other widgets draw inside.
# package: widgets/block
# constructor: block.New(r buffer.Rect) *block.Block
# 120 columns x 9 rows, rendered through widgettest.Capture

╭ Package layout ──────────────────────────────────────────────────────────────────────────────────────────────────────╮
│                                                                                                                      │
│                                                                                                                      │
│                                                                                                                      │
│                                                                                                                      │
│                                                                                                                      │
│                                                                                                                      │
│                                                                                                                      │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯

What you are looking at. This is the cell grid the renderer produced, in a web page. It is not a screenshot of a terminal, and it cannot show interaction, resize or animation — a capture is one settled frame after N frames of settling. Braille and Block Elements can take a different advance width in your browser's font than in a terminal, so if a gauge or a sparkline above looks wrong, the plain-text form below it is the exact output and the colour one is the decoration. The colour frames are at 40, 80, 120 columns; the plain-text frame is at 120. More on this.

Two widgets that hand out rectangles

Widget What it owns What it gives you
Block border, title, background, padding Interior()
Split one solved constraint list, pane focus, a draggable divider each pane’s Bounds()

Split delegates to layout.Solve rather than reimplementing it, and it introduces exactly one interface of its own — Bounded, for a pane that wants to state what it needs.

Split rendered by cmd/capture through widgettest.Capture: colour at 3 widths (40, 80, 120 columns), and the exact plain-text cell grid at the widest of them.

MinSize() returns 3 × 1 cells. That is the whole widget including its own chrome, and it is the widget's assertion about itself — below it, Draw clips rather than blanks, and what to do about that is the application's decision.

40 columns × 11 rows

╭ modules ────────────╮ ╭ packages ────╮
│  module        size │ │   input      │
│  buffer       8.2 … │ │   layout     │
│  geometry     3.1 … │ │>  render    █│
│› headless     11.4… │ │   term      █│
│  layout       6.7 … │ │   virtual   █│
│  render       14.9… │ │   widgets/b…█│
│  virtual      5.0 … │ │   widgets/b…█│
│                     │ │   widgets/d… │
│                     │ │   widgets/f… │
╰─────────────────────╯ ╰──────────────╯

80 columns × 11 rows

╭ modules ────────────────────────────────────╮ ╭ packages ────────────────────╮
│  module           size state                │ │   input                      │
│  buffer         8.2 kB stable               │ │   layout                     │
│  geometry       3.1 kB stable               │ │>  render                    █│
│› headless      11.4 kB beta                 │ │   term                      █│
│  layout         6.7 kB stable               │ │   virtual                   █│
│  render        14.9 kB beta                 │ │   widgets/basic             █│
│  virtual        5.0 kB alpha                │ │   widgets/block             █│
│                                             │ │   widgets/data               │
│                                             │ │   widgets/form               │
╰─────────────────────────────────────────────╯ ╰──────────────────────────────╯

120 columns × 11 rows

╭ modules ────────────────────────────────────────────────────────────╮ ╭ packages ────────────────────────────────────╮
│  module           size state                                        │ │   input                                      │
│  buffer         8.2 kB stable                                       │ │   layout                                     │
│  geometry       3.1 kB stable                                       │ │>  render                                    █│
│› headless      11.4 kB beta                                         │ │   term                                      █│
│  layout         6.7 kB stable                                       │ │   virtual                                   █│
│  render        14.9 kB beta                                         │ │   widgets/basic                             █│
│  virtual        5.0 kB alpha                                        │ │   widgets/block                             █│
│                                                                     │ │   widgets/data                               │
│                                                                     │ │   widgets/form                               │
╰─────────────────────────────────────────────────────────────────────╯ ╰──────────────────────────────────────────────╯

Plain text — the exact cell grid, trimmed per line. This is the honest form: Braille and block-element glyphs can take a different advance width in a web font than a terminal gives them, which breaks the alignment they depend on.

# Split — A container that divides its rectangle among focusable panes.
# package: widgets/split
# constructor: split.New(d layout.Direction, panes ...termmosaic.Widget) *split.Split
# 120 columns x 11 rows, rendered through widgettest.Capture

╭ modules ────────────────────────────────────────────────────────────╮ ╭ packages ────────────────────────────────────╮
│  module           size state                                        │ │   input                                      │
│  buffer         8.2 kB stable                                       │ │   layout                                     │
│  geometry       3.1 kB stable                                       │ │>  render                                    █│
│› headless      11.4 kB beta                                         │ │   term                                      █│
│  layout         6.7 kB stable                                       │ │   virtual                                   █│
│  render        14.9 kB beta                                         │ │   widgets/basic                             █│
│  virtual        5.0 kB alpha                                        │ │   widgets/block                             █│
│                                                                     │ │   widgets/data                               │
│                                                                     │ │   widgets/form                               │
╰─────────────────────────────────────────────────────────────────────╯ ╰──────────────────────────────────────────────╯

What you are looking at. This is the cell grid the renderer produced, in a web page. It is not a screenshot of a terminal, and it cannot show interaction, resize or animation — a capture is one settled frame after N frames of settling. Braille and Block Elements can take a different advance width in your browser's font than in a terminal, so if a gauge or a sparkline above looks wrong, the plain-text form below it is the exact output and the colour one is the decoration. The colour frames are at 40, 80, 120 columns; the plain-text frame is at 120. More on this.

Nested composition

Nesting is running Solve on a sub-rectangle. There is no nesting engine and no container widget with children, so a grid is a Split inside a Split:

rows := split.New(layout.DirectionColumn,
    split.New(layout.DirectionRow, leftPane, rightPane),
    statusBar,
)

Each Split solves its own axis and hands each child a rectangle. If you find yourself writing a third level of nesting, the constraint list is probably doing more work than the pane model is.

The five composition mistakes

Sizing the inner widget from the outer rect. field.W = s.bounds.W - 4 is hand-rolled knowledge of a border and a padding, and it is wrong the moment you change either. Use Interior().

Drawing your own border. The moment a second one exists you have two implementations, and examples/hello had exactly that bug — its own border runes with its own W<4 and W<16 guards. Composing a Block deleted it.

Forgetting to repaint. If you do not compose a Block in Background, your widget must fill its own rect before drawing into it, or shrinking leaves stale cells. This is ADR 0007 §1 rule 3 and it is silent when you get it wrong.

Solving against s.bounds and then adding padding yourself. Same mistake as the first one, in a different place.

Setting Block’s bounds only at construction. Draw must set them every frame, because the application recomputes them on a resize and a Block whose rectangle were stale would paint chrome in the wrong place. examples/hello does it in Draw for that reason.

Centring a panel

The idiom for “as big as I want, never bigger than the screen, centred”:

func centredOnAxis(n int) []layout.Constraint {
    return []layout.Constraint{layout.Fill(1), layout.Max(n), layout.Fill(1)}
}

xs := layout.Solve(layout.Horizontal, centredOnAxis(46), 0, sw)
ys := layout.Solve(layout.Vertical, centredOnAxis(9), 0, sh)

Max is bounded by the space it is measured against, so the result cannot overflow the screen and needs no clipping afterwards. The two Fill(1) are the centring — leftover space shared equally — and when there is none they get nothing, which is the clamp, expressed rather than hand-written.

This replaces the centred(sw, sh, 46, 9) shape that ADR 0007 was written about: hard-coded size, hand-rolled clamping, and centre-of-screen arithmetic that no amount of shrinking made correct. At 20×8 the old code produced a block whose body rows were silently cut off; at 10×4 it lost the title and the body. Nothing crashed and nothing was useful.

One consequence, recorded so nobody reads it as a bug: leftover space is distributed by largest remainder, which hands a leftover cell to the earliest-declared Fill. On odd slack the panel sits one row lower than dead centre.

Reading next