-
Notifications
You must be signed in to change notification settings - Fork 0
Layout and style
Terminal layout is geometry, not pixels. glyphora divides the available cell grid with constraints, then renders each child inside the rectangle it receives. Once that rule clicks, layouts stay predictable through resizes and nested panels.
Core idea: a
rowdivides width, acolumndivides height, and a constraint on each child says how much of that axis it claims.
column(
topBar("deployctl").length(1),
row(
panel("Services")(serviceList).length(28),
panel("Details")(details).fill,
).fill,
statusBar(bindings).length(1),
)Read it from the outside in:
- the outer
columnreserves one row for the top bar and one for the status bar; - its middle row uses
.fill, so it receives all remaining height; - inside that row, the service panel receives 28 columns and details fills the rest.
panel, text, inputs, and other elements have sensible preferred sizes. Add an
explicit constraint only when the surrounding composition needs one.
| Extension | Meaning | Typical use |
|---|---|---|
.length(12) |
exactly 12 cells | toolbars, sidebars, single-line regions |
.percent(40) |
40% of available axis | balanced master/detail layouts |
.fill |
share all space left after fixed constraints | main content |
.fill(2) |
take twice the remaining share of .fill(1)
|
weighted columns |
.minSize(8) |
at least 8 cells when solving | important compact content |
.maxSize(30) |
no more than 30 cells | readable text or narrow controls |
Weighted fills make proportions clear without hardcoding terminal width:
row(
panel("Queue")(queue).fill(1),
panel("Timeline")(timeline).fill(2),
panel("Health")(health).fill(1),
).gap(1)centered(width, height) is convenient for dialogs and focused empty states:
centered(42, 9) {
panel("No deployments")(
text("Connect a cluster to begin.").bold,
text("Press c to configure one.").dim,
).rounded
}For independent horizontal and vertical alignment, use place:
place(
width = 36,
height = 5,
horizontal = Align.End,
vertical = Align.Start,
)(toastPreview)App-oriented presets cover frequent shapes:
sidebarLayout(navigation, content, sideWidth = 26)
masterDetail(projectList, projectDetails, masterWidth = 32)Rows and columns support flex-like packing when their children do not consume all available space:
row(
button("Cancel", cancel),
button("Deploy", deploy),
).gap(2).flexEndAvailable modes are .center, .spaceBetween, .spaceAround, .spaceEvenly, and
.flexEnd. They matter only when space remains; a .fill child intentionally
consumes that space first.
Styling calls return a new element, so they chain naturally and never mutate a shared widget:
text("production")
.bold
.color(Color.White)
.background(Color.Red)
panel("Audit log")(logView).rounded
panel("Danger zone")(dangerView).doubleBorder.color(Color.Red)The built-in modifiers are .bold, .dim, .italic, .underline, .reverse,
.color(...), and .background(...). Use .styled when you need a complete
Style transformation.
Apply a base style to a whole subtree with withStyle:
withStyle(_.withFg(Color.Cyan)) {
column(
text("connected").bold,
text("latency 12 ms"),
)
}Descendants can still add or override their own style. Raw widget(...) leaves and
images intentionally ignore the element style because their renderer owns its
cells directly.
For application chrome and reusable components, draw from the ambient Theme:
def deploymentStatus(name: String, healthy: Boolean)(using theme: Theme): Element =
val tone = if healthy then theme.success else theme.error
text(s"● $name").styled(_ => tone)Theme.Dark, Theme.Light, and Theme.HighContrast are built in. A custom theme is
just a value containing semantic styles (primary, accent, muted, error,
warning, success, surface, border, and focus). See The app shell
for live switching.
- A constraint applies along the parent container's direction:
.length(10)is width in a row and height in a column. - Borders consume cells. A 3-row panel has one inner row after its top and bottom border.
- Use
CharWidth, notString.length, when custom code measures visible text. - Give interactive elements a stable
.key("settings-name")when conditional rendering might otherwise move focus to a different positional index. - Deep fixed sizes fail on small terminals. Reserve fixed cells for chrome, then let primary content fill.
Next, browse the Widget catalog or assemble these pieces into The app shell.
Documentation is maintained in website/docs. Read the styled guide · API reference · MIT license