3. Layout
May 15, 2026 · View on GitHub
Two containers cover 95 % of TUI layouts: Flex for linear stacks,
Grid for tables and dashboards. The third concept — Hint(w, h) —
controls how each child negotiates for space.
Hint values
.Hint(width, height) sets a widget's preferred size. Each axis follows
the same three-case rule:
| Value | Meaning | Example |
|---|---|---|
>0 | Fixed — exactly this many cells | .Hint(20, 1) — exactly 20 cells wide, 1 tall |
<0 | Fractional — share remaining space proportional to magnitude | .Hint(-1, 0) — gets 1× share; sibling with -2 gets 2× |
0 | Auto — ask the widget for its preferred size | .Hint(0, 0) — Static's natural width = its text length |
Run examples/03-layout-hints to see
all three side-by-side. The key file:
HFlex("row1", core.Stretch, 1).Hint(0, 1).
Static("a", "fixed 10").Hint(10, 0).
Static("b", "fixed 20").Hint(20, 0).
Static("c", "auto"). // no Hint → auto
End().
HFlex("row2", core.Stretch, 1).Hint(0, 1).
Static("d", "weight -1").Hint(-1, 0).
Static("e", "weight -2").Hint(-2, 0).
Static("f", "weight -3").Hint(-3, 0). // gets half of the row
End().
The mixed case is common and works the way you'd hope: fixed children take their cells first, then fractional children divide the leftover space by weight.
Flex
HFlex(id, alignment, spacing) // children laid out left-to-right
VFlex(id, alignment, spacing) // children laid out top-to-bottom
alignmentcontrols the cross-axis (vertical for HFlex, horizontal for VFlex). Values fromcore:Start,End,Center,Stretch,Left,Right,Default.spacingis the gap (in cells) between consecutive children.
A typical app shell:
VFlex("root", core.Stretch, 0).
HFlex("header", core.Center, 0).Hint(0, 1).Background("$bg2").
Static("title", "App Name").Font("bold").
End().
HFlex("body", core.Stretch, 0).Hint(0, -1). // -1 height → fill
Static("sidebar", "…").Hint(24, 0). // fixed 24 wide
Static("content", "…").Hint(-1, 0). // fill rest
End().
HFlex("footer", core.End, 2).Hint(0, 1).
Static("hint1", "[q] quit").
End().
End()
Full version: examples/03-layout-flex.
Common Flex mistake: forgetting End()
End() pops the current container off the builder stack. Skip it once and
the next widget you add lands as a child of the previous container
instead of a sibling. The compiler can't catch this — the symptom is a
wonky layout. When something nests where you didn't expect, count your
End()s first.
Grid
Grid(id, rows, columns, lines).
Columns(c1, c2, …). // size of each column
Rows(r1, r2, …). // size of each row
Cell(x, y, w, h).Static(…). // place next widget
Cell(…).Button(…).
End()
rows,columnsare the number of tracks. They must match what you pass toRows()andColumns().lines = truedraws thin separators between cells.Cell(x, y, w, h)— column indexx, row indexy, spanningwcolumns andhrows. Call it before each widget you place.
Track sizes follow the same Hint rules: positive = fixed, negative = fractional, zero = auto. Mix freely:
$\text{go} \text{Columns}(20, -1, -2) // 20 \text{fixed} | 1 \times \text{share} | 2 \times \text{share} \text{Rows}(3, -1, 1) // 3 \text{fixed} | \text{fill} | 1 \text{fixed} $
Full example with row/column spanning:
examples/03-layout-grid.
Grid pitfall: no fractional track
If every column is fixed and the terminal is wider, the leftover space sits
unused on the right. Same for rows. The fix is to give at least one track
on each axis a negative weight (commonly -1).
Margin, border, padding
The CSS-like model. Order from outside to inside is:
┌── margin ─────────────────────────────────┐
│ ┌── border ───────────────────────────┐ │
│ │ ┌── padding ─────────────────────┐ │ │
│ │ │ │ │ │
│ │ │ content │ │ │
│ │ │ │ │ │
│ │ └─────────────────────────────────┘ │ │
│ └─────────────────────────────────────┘ │
└───────────────────────────────────────────┘
Hint(w, h) describes the content box — margin, border, and padding
are added on top by the framework. So .Hint(20, 1).Padding(0, 2) claims
24 cells of horizontal space (20 + 2×2 padding).
Both .Padding(...) and .Margin(...) accept 1-4 ints, CSS-style:
.Padding(1) // 1 on all four sides
.Padding(1, 2) // top/bottom = 1, left/right = 2
.Padding(1, 2, 3) // top = 1, left/right = 2, bottom = 3
.Padding(1, 2, 3, 4) // top, right, bottom, left
.Border(style) accepts the names registered by your theme: "none",
"thin", "thick", "round", "double", "lines". The selector form
.Border(":focus", "double") applies only when the widget is focused.
Spacers and rules
Two cosmetic helpers worth knowing early:
Spacer()— an invisible flexible widget. In an HFlex, drop one between two groups to push them to opposite sides.HRule(style)/VRule(style)— a 1-cell separator line.
HFlex("toolbar", core.Start, 1).
Button("save", "Save").
Button("open", "Open").
Spacer().
Button("quit", "Quit").
End()
Try it
go run ./examples/03-layout-flex
go run ./examples/03-layout-grid
go run ./examples/03-layout-hints
Resize your terminal while each one runs — fractional tracks redistribute, fixed ones don't.