go-tui Project Guidelines
A declarative terminal UI framework for Go with templ-like syntax and flexbox layout.
Git Commits IMPORTANT
Use gcommit -m "" for all commits to ensure proper signing.
ONLY EVER COMMIT USING THIS APPROACH
All commit messages MUST use conventional commit format. This is required for
automated releases via release-please.
Format: <type>: <description> or <type>(<scope>): <description>
| Prefix | Version Bump | Use When |
|---|---|---|
feat: |
minor (0.1.0 → 0.2.0) | Adding new functionality |
fix: |
patch (0.1.0 → 0.1.1) | Fixing a bug |
perf: |
patch | Performance improvements |
refactor: |
patch | Code changes that don't add features or fix bugs |
docs: |
patch | Documentation only |
test: |
patch | Adding or updating tests |
chore: |
patch | Maintenance, dependencies, tooling |
ci: |
patch | CI/CD changes |
build: |
patch | Build system changes |
revert: |
patch | Reverting a previous commit |
For BREAKING CHANGES (major bump, e.g. 0.1.0 → 1.0.0), add ! after the type:
feat!: remove deprecated API or include BREAKING CHANGE: in the commit body.
Pull Requests IMPORTANT
PR titles MUST pass the GitHub Actions checks before merging. There are two CI
workflows that run on every PR:
- PR Title (
pr-title.yml): Enforces conventional commit format on the PR
title usingaction-semantic-pull-request. The title must start with one of
the allowed types (feat,fix,docs,chore,ci,test,refactor,
perf,build,revert). This is the same format as commit messages. - CI (
ci.yml): Runsgo test ./...on ubuntu-latest.
Both checks must pass. Always verify your PR title matches the conventional
commit format and that tests pass before considering a PR ready.
Examples:
gcommit -m "feat: add table element support"
gcommit -m "fix(layout): correct flexbox gap calculation"
gcommit -m "feat!: change State API to require type parameter"
gcommit -m "chore: update golang.org/x dependencies"
Code Style & Linting
We enforce strict formatting, linting, and language modernization standards for all Go files:
- Formatting: Format all Go files strictly using
golangci-lint fmt:golangci-lint fmt - Modernization: Use
go fixto apply and keep modern Go library usages and constructs updated:go fix ./... - Linting: Verify code quality and static analysis warnings locally using
golangci-lint:golangci-lint run - Template Formatting: Format custom
.gsxtemplate files using:tui fmt [path...]
Project Overview
go-tui allows defining UIs in .gsx files that compile to type-safe Go code. The framework provides:
- Declarative component syntax (similar to templ/JSX)
- Pure Go flexbox layout engine (no CGO)
- Minimal external dependencies (golang.org/x/{mod,sync,sys,tools})
- Composable widget system
- Reactive state management with generic
State[T] - Language server, formatter, and tree-sitter grammar for editor support
Architecture
.gsx files (declarative syntax)
│ tui generate (internal/tuigen)
▼
Generated Go code (*_gsx.go)
│ imports tui "github.com/grindlemire/go-tui"
▼
Widget Tree + Layout Engine (internal/layout)
│
▼
Character Buffer (double-buffered 2D grid)
│
▼
Terminal (ANSI escape sequences)
All public API types live in the root tui package. Internal packages (internal/layout,
internal/tuigen, internal/formatter, internal/lsp, internal/debug) are not importable
by external consumers.
Where to Look (By Task)
Use this section to quickly find the right files for a given change.
Changing the public API (Element options, App config, types)
element.go— Element struct definition, TextAlign/ScrollMode/OverflowMode enumselement_options.go— Option funcs: WithWidth, WithHeight, WithFlexGrow, WithDirection, WithBorder, WithScrollable, WithTruncate, WithHidden, WithOverflow, WithTextGradient, WithBackgroundGradient, WithBorderGradient, WithBorderTitleAlign, WithBorderTitleStyle, WithFocusBorderStyle, WithCursorSource, etc.element_options_auto.go— WithWidthAuto(), WithHeightAuto()element_accessors.go— Getters/setters: SetText, SetBorder, SetStyle, Background, SetWidth, SetHeight, SetBorderTitleAlign, SetBorderTitleStyle, SetFocusBorderStyle, etc.element_tree.go— Tree manipulation: AddChild, RemoveChild, RemoveAllChildrenelement_scroll.go— Scroll methods: ScrollTo, ScrollOffset, MaxScroll, ViewportSizeapp_options.go— AppOption funcs: WithFrameRate, WithMouseEnabled, WithInlineHeight, WithGlobalKeyHandler, WithInputLatency, WithEventQueueSize, WithPreRenderHook, WithPostRenderHook, WithManualCursor, etc.layout.go— Re-exports from internal/layout (Direction, Justify, Align, Value, etc.)color.go— Color type, ANSIColor(), RGBColor(), HexColor(), Gradient, GradientDirectionstyle.go— Style type with chainable methods (Bold, Dim, Italic, Foreground, etc.)ref.go— Ref, RefList, RefMap[K] for element referencesclick.go— Click(), HandleClicks() for ref-based mouse hit testingkeymap.go— KeyMap, KeyBinding, KeyPattern, On(), OnStop(), Rune(), etc.component.go— Component, KeyListener, MouseListener, Initializer, WatcherProvider interfacesevents.go— Events[T] generic event bus for cross-component communicationmount.go— Component caching and lifecycle (Mount, PropsUpdater)modal.go-- Modal struct, Render, KeyMap (Escape/Tab/Enter/catch-all), HandleMouse (backdrop + onActivate click)modal_options.go-- ModalOption funcs: WithModalOpen, WithModalBackdrop, WithModalCloseOnEscape, WithModalCloseOnBackdropClick, WithModalTrapFocus, WithModalElementOptionsapp_overlay.go-- Overlay registration: overlayEntry, registerOverlay, clearOverlays
Changing layout behavior (flexbox algorithm)
internal/layout/calculate.go— Main layout algorithm,Calculate()entry pointinternal/layout/flex.go— Core flexbox:layoutChildren()6-phase algorithm, justify/align offsetsinternal/layout/style.go— LayoutStyle struct, Direction/Justify/Align constantsinternal/layout/value.go— Value type: Auto/Fixed/Percent dimension unitsinternal/layout/edges.go— Edges struct (TRBL spacing), EdgeAll(), EdgeSymmetric()internal/layout/layoutable.go— Layoutable interface (LayoutStyle, LayoutChildren, SetLayout, etc.)element_layout.go— Element's implementation of Layoutable interface
Changing rendering / terminal output
element_render.go— RenderTree(), renderElement(): border drawing, text rendering, child recursionrender.go— Render(), RenderFull(), RenderRegion(): diff computation, full redrawbuffer.go— Double-buffered character grid: front/back, Diff(), Swap(), SetCell(), Fill()cell.go— Cell struct: Rune, Style, Width (CJK support)render_element.go— renderElementToBuffer, bufferRowToANSI: standalone element-to-ANSI renderingescape.go— escBuilder: ANSI escape sequence construction (cursor, colors, styles)terminal.go— Terminal interface definitionterminal_ansi.go— ANSITerminal implementation (ANSI escape output, capabilities)terminal_unix.go— Unix raw mode via termios syscallsterminal_windows.go— Windows raw modecaps.go— DetectCapabilities(): TERM/COLORTERM env var detectionborder.go— BorderStyle type and BorderChars definitionsinline_ansi.go— ANSI escape handling for inline modeinline_session.go— Inline session managementinline_wrap.go— Inline content wrapping
Changing event handling / input
event.go— Event interface, KeyEvent, MouseEvent (MouseButton, MouseAction), ResizeEvent typeskey.go— Key type: special keys (Escape, Enter, Tab, arrows, Ctrl+A-Z, function keys)keymap.go— KeyMap, KeyBinding, KeyPattern; helpers: On(), OnStop(), OnPreemptStop(), OnFocused(), Rune(), AnyRune, AnyKeydispatch.go— Key dispatch table: priority-ordered key binding dispatch by tree positionparse.go— parseInput(): CSI/SS3 sequence parsing, mouse SGR, UTF-8, modifiersreader.go— EventReader/InterruptibleReader interfaces, stdinReader, PollEvent()reader_types.go— EventReader and InterruptibleReader interface definitionsreader_unix.go— Unix-specific: selectWithTimeout(), selectWithTimeoutAndInterrupt()reader_windows.go— Windows-specific terminal inputclick.go— ClickBinding, Click(), HandleClicks() for ref-based mouse hit testingapp_events.go— App.Dispatch(): routes ResizeEvent, MouseEvent, key events via FocusManagerapp_resize_unix.go— SIGWINCH handler: registerResizeSignal(), dispatches ResizeEvent via event queueapp_resize_windows.go— No-op (Windows has no SIGWINCH)
Changing focus management
focus.go— Focusable interface, FocusManager: Register(), Next(), Prev(), SetFocus(), Dispatch()focus_group.go— FocusGroup for Tab/Shift+Tab cycling between componentselement_focus.go— Element focus API: IsFocusable, Focus(), Blur(), HandleEvent(), SetOnEvent()element_watchers.go— Focus tree discovery: WalkFocusables(), SetOnFocusableAdded()
Changing reactive state / watchers
state.go— State[T] generic type: Bind(), Set(), Get(), Batch(), dirty markingwatcher.go— Watcher interface, Watch() helper, TimerWatcher, TickerWatcherelement_watchers.go— AddWatcher(), Watchers(), WalkWatchers(), SetOnUpdate()dirty.go— Global dirty flag: MarkDirty(), checkAndClearDirty()
Changing the app lifecycle / main loop
app.go— App struct, Viewable interfaceapp_loop.go— Run() main event loop, frame timing, signal handlingapp_lifecycle.go— Close(), Stop(), PrintAbove(), PrintAboveElement(), StreamAbove() for inline modeapp_render.go— App.Render(): buffer management, dirty checking, inline vs full-screenapp_screen.go— Alternate screen mode API (EnterAlternateScreen, ExitAlternateScreen)app_inline_startup.go— Inline screen mode initialization and startup policymount.go— Component caching with mark-and-sweep lifecycle (Mount, PropsUpdater)
Changing the .gsx compiler (code generation)
The compiler pipeline is: Lexer → Parser → Analyzer → Generator
internal/tuigen/token.go— TokenType enum, Token struct, Position- Lexer (
internal/tuigen/):lexer.go— Main Lexer struct, NewLexer(), Next()lexer_utils.go— Helper functions (readIdent, readString, isLetter)lexer_goexpr.go— Go expression lexing inside{...}lexer_strings.go— String literal parsing (double-quoted, backtick)
- Parser (
internal/tuigen/):parser.go— Parser struct, NewParser(), ParseFile(), error synchronizationparser_component.go— Component definition and parameter parsingparser_element.go— Element parsing: attributes, children, self-closing tagsparser_control.go— Control flow: for, if, else, let/:= bindingsparser_expr.go— Go expression parsing within elements/attributes
- AST (
internal/tuigen/):ast.go— 20+ node types: File, Component, Element, Attribute, ForLoop, IfStmt, LetBinding, GoExpr, ComponentCall, ChildrenSlot, GoFunc, GoDecl, etc.
- Analyzer (
internal/tuigen/):analyzer.go— Semantic analysis: validates tags, attributes, importsanalyzer_refs.go— Named reference tracking and validationanalyzer_state.go— State variable and binding tracking
- Generator (
internal/tuigen/):generator.go— Generator struct, NewGenerator(), Generate(), imports processinggenerator_element.go— Element code generation with option building, ref handlinggenerator_children.go— Child element generation, text handlinggenerator_component.go— Component function generationgenerator_control.go— Control flow code generation (for, if, let/:= bindings)
- Tailwind (
internal/tuigen/):tailwind.go— ParseTailwindClasses(): converts class strings to element optionstailwind_validation.go— Class syntax validationtailwind_data.go— Class definitions and mappingstailwind_autocomplete.go— Autocomplete suggestions for classes
errors.go— Error struct with position/message/hint, ErrorList
Changing the formatter
internal/formatter/formatter.go— Formatter struct, New(), Format(), FormatWithResult()internal/formatter/printer.go— printer struct: generates formatted .gsx from ASTinternal/formatter/printer_comments.go— Comment formatting (leading/trailing/orphan)internal/formatter/printer_control.go— Control structure formatting (if, for, let/:= bindings)internal/formatter/printer_elements.go— Element/attribute formatting, multi-line layoutinternal/formatter/imports.go— Import fixing via goimports integration
Changing the LSP (language server)
- Core (
internal/lsp/):server.go— Server struct, JSON-RPC communication, document managementrouter.go— Request dispatch to handlers and providershandler.go— LSP lifecycle: initialize, shutdown, exitdocument.go— Document/DocumentManager for open .gsx file trackingcontext.go— CursorContext: resolved AST info at cursor positioncontext_resolve.go— Cursor position resolution logicindex.go— ComponentIndex: workspace-level symbol indexingproviders.go— Provider interface definitions, Registry structprovider_adapters.go— Adapters between lsp and provider package types
- Feature files (
internal/lsp/):hover.go,completion.go,definition.go,references.godiagnostics.go,formatting.go,semantic_tokens.go,symbols.go
- Provider implementations (
internal/lsp/provider/):provider.go— Provider interface definitions, type aliasescompletion.go/completion_items.go— Completion providerhover.go— Hover documentation providerdefinition.go/definition_search.go— Go-to-definitionreferences.go/references_search.go— Find referencesdiagnostics.go— Diagnostic reportingformatting.go— Document formattingsemantic.go/semantic_gocode.go/semantic_nodes.go— Semantic tokenssymbols.go— Document and workspace symbols
- gopls proxy (
internal/lsp/gopls/):proxy.go— GoplsProxy: subprocess communication over JSON-RPCproxy_requests.go— JSON-RPC request typesmapping.go— SourceMap: position translation between .gsx and generated .gogenerate.go/generate_state.go— Generate virtual Go from .gsx AST for gopls
- Schema (
internal/lsp/schema/):schema.go— Element definitions (div, span, p, ul, li, button, input, table, progress, hr, br)keywords.go— Keyword definitions (templ, for, if, else, :=)tailwind.go— Tailwind class documentation and matching
- Logging (
internal/lsp/log/):log.go— Logging with component prefixes (Server, Gopls, Generate, Mapping)
Changing the CLI tool
cmd/tui/main.go— Entry point, version, CLI dispatchercmd/tui/generate.go—tui generatecommandcmd/tui/check.go—tui checkcommandcmd/tui/fmt.go—tui fmtcommand (supports --stdout, --check)cmd/tui/lsp.go—tui lspcommand (supports --log FILE)
Adding or modifying element types / attributes
internal/lsp/schema/schema.go— Element definitions and attribute schemasinternal/tuigen/analyzer.go— Attribute validation rulesinternal/tuigen/tailwind.go— Tailwind class → option mappingelement_options.go— New Option funcs for generated code to call
Changing modal/overlay behavior
modal.go-- Modal struct, Render, KeyMap, HandleMousemodal_options.go-- ModalOption funcsapp_overlay.go-- Overlay registration (overlayEntry, registerOverlay, clearOverlays); inline mode guardapp_render.go:67-106-- Overlay render pass: focus scoping, backdrop, renderelement.go:103-106-- Overlay flag on Elementelement_options.go-- WithOverlay optionelement_layout.go-- Overlay children filtered from layoutelement_render.go-- Overlay children skipped in normal renderelement_focus.go-- Auto focus border highlighting, onActivatebuffer.go-- ApplyDim, FillBlank for backdrop effectsfocus.go-- ScopeTo, ClearScope, isInScope for focus trappingdispatch.go-- Preemptive dispatch pass for modal key blockingkeymap.go-- OnPreemptStop, AnyKey, Preempt flag on KeyBinding- Note: Modals are not supported in inline mode.
registerOverlaysilently skips
the overlay and logs viadebug.LogwheninlineHeight > 0and the app is not in
alternate screen mode. UseEnterAlternateScreen()first.
Changing markdown rendering
markdown.go-- Markdown component: struct, NewMarkdown, BindApp, UpdateProps, Render, block-walk methods (renderHeading/renderParagraph/renderCodeFence/renderList/renderBlockquote/renderTable), inlineToSpans, single-entry parse cachemarkdown_options.go-- MarkdownOption funcs: WithMarkdownSource, WithMarkdownState, WithMarkdownWidth, WithMarkdownThememarkdown_theme.go-- MarkdownTheme struct and DefaultMarkdownTheme()internal/markdown/-- zero-dependency parser: Parse() returns a recursive []Block tree (no tui import)richtext.go-- TextSpan/WithRichText primitive that headings/paragraphs/table cells render throughhighlight.go-- CodeHighlighter interface, TokenKind, Palette, DefaultPalette,
NewHighlighter (built-in), builtinHighlighter; maps internal/highlight tokens to colored TextSpansinternal/highlight/-- zero-dependency syntax tokenizer (no tui import): Tokenize returns
[][]Token per line; per-language lexers for Go, JSON, Bash, JS/TS; unknown languages fall back to plain- gsx integration:
internal/tuigen/generator_element.go(isComponentElement, componentConstructor, markdownAttributeToOption),internal/tuigen/analyzer.go(knownTags, voidElements, knownAttributes: source/state/theme),internal/lsp/schema/schema.go(markdownAttrs) - Note:
<markdown>is self-closing; content comes fromsource/stateexpression attributes (the generator cannot type-discriminate, so source and state are distinct attributes). The component owns no scroll/keys; wrap it in a scrollable container. - Note: Fenced code blocks are syntax-highlighted by default for Go, JSON, Bash, and
JS/TS via the theme'sCodeHighlighter. Settheme.CodeHighlighter = nilto
disable, ortui.NewHighlighter(customPalette)to recolor. Unknown languages
render uncolored. ImplementCodeHighlighterto plug in another engine (e.g. a
chroma adapter using chroma's lexer, mapped to per-line[]TextSpan).
Writing tests
mock_terminal.go— MockTerminal: captures operations, maintains internal cell buffermock_reader.go— MockEventReader: returns queued events in ordercmd/tui/testdata/— .gsx fixtures and their expected generated .go output
CLI Commands
tui generate [path...] # Generate Go code from .gsx files
tui check [path...] # Check .gsx files without generating
tui fmt [path...] # Format .gsx files
tui fmt --check [path...] # Check formatting without modifying
tui fmt --stdout [path...] # Write formatted output to stdout
tui lsp # Start language server (stdio)
tui lsp --log FILE # Start language server with logging
tui version # Print version
tui help # Show help
.gsx File Syntax
package mypackage
import (
"fmt"
)
// Component definition (returns Element)
templ Header(title string) {
<div class="border-single p-1">
<span class="font-bold">{title}</span>
</div>
}
// Conditionals
templ Conditional(show bool) {
<div class="flex-col">
if show {
<span>Visible</span>
} else {
<span>Hidden</span>
}
</div>
}
// Loops
templ List(items []string) {
<div class="flex-col gap-1">
for i, item := range items {
<span>{fmt.Sprintf("%d: %s", i, item)}</span>
}
</div>
}
// Local bindings (element references)
templ Counter(count int) {
label := <span class="font-bold">{fmt.Sprintf("Count: %d", count)}</span>
<div>{label}</div>
}
// Component calls
templ App() {
<div class="flex-col">
@Header("Hello")
@Counter(0)
</div>
}
// Helper functions (regular Go - no Element return type)
func helper(s string) string {
return fmt.Sprintf("[%s]", s)
}
Built-in Elements (HTML-style)
| Element | Description |
|---|---|
<div> |
Block container with flexbox layout |
<span> |
Inline text/content container |
<p> |
Paragraph text |
<ul> |
Unordered list container |
<li> |
List item |
<button> |
Clickable button |
<input> |
Text input field |
<textarea> |
Multi-line text input (self-closing, mounted as Component) |
<table> |
Table container |
<tr> |
Table row |
<td> |
Table data cell |
<th> |
Table header cell (bold by default) |
<progress> |
Progress bar |
<hr> |
Horizontal rule (self-closing) |
<br> |
Line break (self-closing) |
<modal> |
Modal overlay dialog |
<markdown> |
Renders a markdown string into the widget tree (self-closing) |
Common Attributes
| Attribute | Type | Description |
|---|---|---|
id |
string |
Unique identifier |
class |
string |
Tailwind-style classes |
disabled |
bool |
Disable interaction |
ref |
expression | Bind element to a tui.Ref/RefList/RefMap variable |
key |
expression | Stable identity for a loop item, unique among siblings of the innermost loop (like React keys): mount cache key for component elements, RefMap key with ref; a key on a container element keys the components mounted inside it |
deps |
expression | Explicit state dependencies for reactive bindings |
Layout Attributes
| Attribute | Type | Description |
|---|---|---|
width |
int |
Fixed width in characters |
widthPercent |
int |
Width as percentage |
height |
int |
Fixed height in rows |
heightPercent |
int |
Height as percentage |
minWidth |
int |
Minimum width |
minHeight |
int |
Minimum height |
maxWidth |
int |
Maximum width |
maxHeight |
int |
Maximum height |
direction |
tui.Direction |
Flex direction |
justify |
tui.Justify |
Main axis alignment |
align |
tui.Align |
Cross axis alignment |
alignSelf |
tui.Align |
Override parent's align for this item |
gap |
int |
Gap between children |
flexGrow |
float64 |
Flex grow factor |
flexShrink |
float64 |
Flex shrink factor |
padding |
int |
Padding on all sides |
margin |
int |
Margin on all sides |
Visual Attributes
| Attribute | Type | Description |
|---|---|---|
border |
tui.BorderStyle |
Border style |
borderStyle |
string |
Border style name |
borderTitle |
string |
Title text centered in the top border |
background |
tui.Color |
Background color |
text |
string |
Text content |
textStyle |
tui.Style |
Text styling |
textAlign |
string |
Text alignment |
Event & Focus Attributes
| Attribute | Type | Description |
|---|---|---|
focusable |
bool |
Whether the element can receive focus |
onFocus |
func() |
Called when the element gains focus |
onBlur |
func() |
Called when the element loses focus |
onActivate |
func() |
Called when Enter is pressed while focused |
Scroll Attributes
| Attribute | Type | Description |
|---|---|---|
scrollable |
bool |
Enable scrolling for overflow content |
scrollbarStyle |
tui.Style |
Style for the scrollbar track |
scrollbarThumbStyle |
tui.Style |
Style for the scrollbar thumb |
hideScrollbar |
bool |
Hide the scrollbar and reclaim its gutter width |
Modal Attributes
| Attribute | Type | Description |
|---|---|---|
open |
expression |
Bind to a *State[bool] to control visibility (required) |
backdrop |
string |
Backdrop style: "dim" (default), "blank", or "none" |
closeOnEscape |
bool |
Escape key closes the modal (default true) |
closeOnBackdropClick |
bool |
Clicking backdrop closes the modal (default true) |
trapFocus |
bool |
Tab/Shift+Tab restricted to modal children; also blocks unhandled keys from parents (default true) |
keyMap |
expression |
Custom KeyMap bindings for the modal; fire before the catch-all when trapFocus is true |
Input-specific Attributes
| Attribute | Type | Description |
|---|---|---|
value |
string |
Current input value |
placeholder |
string |
Placeholder text when empty |
Progress-specific Attributes
| Attribute | Type | Description |
|---|---|---|
value |
int |
Current progress value (0 to max) |
max |
int |
Maximum progress value |
Textarea-specific Attributes
| Attribute | Type | Description |
|---|---|---|
placeholder |
string |
Placeholder text when empty |
width |
int |
Width in characters (default 40) |
maxHeight |
int |
Maximum height in rows (0 = unlimited) |
border |
tui.BorderStyle |
Border style |
textStyle |
tui.Style |
Text styling |
placeholderStyle |
tui.Style |
Placeholder styling (default: dim) |
cursor |
rune |
Cursor character (default '▌') |
submitKey |
tui.Key |
Submit trigger key (default KeyEnter) |
onSubmit |
func(string) |
Called when submit key is pressed |
Markdown-specific Attributes
| Attribute | Type | Description |
|---|---|---|
source |
string |
Static markdown content (string expression) |
state |
*State[string] |
Reactive markdown source; re-renders on change |
width |
int |
Fixed render width in characters (0 = fill available width) |
theme |
tui.MarkdownTheme |
Override the default styling |
The <markdown> tag is self-closing; provide content through source or state
(literal markdown as children is not supported). Markdown is a pure content
renderer, so wrap it in a scrollable container to scroll long documents. It
renders headings, bold/italic, inline code, fenced code blocks, pipe tables,
ordered/unordered lists (with nesting), blockquotes, and links (OSC 8 hyperlinks
on capable terminals).
Tailwind-style Classes
Use the class attribute for styling:
<div class="flex-col gap-2 p-2 border-rounded">
<span class="font-bold text-cyan">Title</span>
<span class="font-dim">Subtitle</span>
</div>
Layout Direction
| Class | Description |
|---|---|
flex / flex-row |
Display flex row |
flex-col |
Display flex column |
Flex Sizing
| Class | Description |
|---|---|
grow |
Allow element to grow (flex-grow: 1) |
grow-0 |
Prevent element from growing |
shrink |
Allow element to shrink (flex-shrink: 1) |
shrink-0 |
Prevent element from shrinking |
flex-1 |
Grow and shrink (takes available space) |
flex-auto |
Grow and shrink (respects content size) |
flex-initial |
Don't grow, can shrink |
flex-none |
Fixed size (no grow or shrink) |
flex-grow-N |
Flex grow factor of N |
flex-shrink-N |
Flex shrink factor of N |
Width & Height
| Class | Description |
|---|---|
w-N |
Fixed width of N characters |
h-N |
Fixed height of N rows |
w-full / h-full |
Full width/height (100%) |
w-auto / h-auto |
Auto size to content |
w-N/M / h-N/M |
Fractional size (e.g., w-1/2 for 50%) |
min-w-N |
Minimum width of N characters |
max-w-N |
Maximum width of N characters |
min-h-N |
Minimum height of N rows |
max-h-N |
Maximum height of N rows |
Justify & Align
| Class | Description |
|---|---|
justify-start |
Justify content to start |
justify-center |
Justify content to center |
justify-end |
Justify content to end |
justify-between |
Justify space between |
justify-around |
Justify space around |
justify-evenly |
Justify space evenly |
items-start |
Align items to start |
items-center |
Align items to center |
items-end |
Align items to end |
items-stretch |
Stretch items to fill |
self-start |
Align self to start |
self-center |
Align self to center |
self-end |
Align self to end |
self-stretch |
Stretch self to fill |
Spacing
| Class | Description |
|---|---|
gap-N |
Gap of N characters between children |
p-N |
Padding of N on all sides |
px-N / py-N |
Horizontal / vertical padding |
pt-N / pr-N / pb-N / pl-N |
Individual side padding |
m-N |
Margin of N on all sides |
mx-N / my-N |
Horizontal / vertical margin |
mt-N / mr-N / mb-N / ml-N |
Individual side margin |
Borders
| Class | Description |
|---|---|
border / border-single |
Single line border |
border-double |
Double line border |
border-rounded |
Rounded border |
border-thick |
Thick border |
border-COLOR |
Border color (red, green, blue, cyan, etc.) |
border-[#hex] |
Border color from hex (e.g., border-[#ff6600]) |
border-gradient-C1-C2[-dir] |
Border gradient (directions: h, v, dd, du) |
Text Styling
| Class | Description |
|---|---|
font-bold |
Bold text |
font-dim / text-dim |
Dim/faint text |
italic |
Italic text |
underline |
Underlined text |
strikethrough |
Strikethrough text |
blink |
Blinking text |
reverse |
Reverse video text |
text-left / text-center / text-right |
Text alignment |
truncate |
Truncate text with ellipsis on overflow |
Colors
| Class | Description |
|---|---|
text-COLOR |
Text color (red, green, blue, cyan, magenta, yellow, white, black) |
text-bright-COLOR |
Bright text color variant |
text-[#hex] |
Text color from hex (e.g., text-[#ff0000]) |
bg-COLOR |
Background color |
bg-bright-COLOR |
Bright background color variant |
bg-[#hex] |
Background color from hex |
Gradients
| Class | Description |
|---|---|
text-gradient-C1-C2[-dir] |
Text gradient between two colors |
bg-gradient-C1-C2[-dir] |
Background gradient between two colors |
border-gradient-C1-C2[-dir] |
Border gradient between two colors |
Gradient directions: -h (horizontal, default), -v (vertical), -dd (diagonal down), -du (diagonal up). Colors: any named color or bright- variant.
Scroll & Overflow
| Class | Description |
|---|---|
overflow-scroll |
Enable scrolling in both directions |
overflow-y-scroll |
Enable vertical scrolling |
overflow-x-scroll |
Enable horizontal scrolling |
overflow-hidden |
Clip children without scrollbars |
scrollbar-hidden |
Hide scrollbar and reclaim its gutter width |
scrollbar-COLOR |
Scrollbar track color |
scrollbar-thumb-COLOR |
Scrollbar thumb color |
scrollbar-[#hex] / scrollbar-thumb-[#hex] |
Hex scrollbar colors |
Other
| Class | Description |
|---|---|
focusable |
Make element focusable |
hidden |
Hide element from layout and rendering |
Key Types
// tui.Value - dimension specification
tui.Fixed(10) // 10 characters
tui.Percent(50) // 50% of available space
tui.Auto() // Size to content
// tui.BorderStyle
tui.BorderNone
tui.BorderSingle // ┌─┐│└─┘
tui.BorderDouble // ╔═╗║╚═╝
tui.BorderRounded // ╭─╮│╰─╯
tui.BorderThick // ┏━┓┃┗━┛
// tui.Direction
tui.Row
tui.Column
// tui.Justify
tui.JustifyStart
tui.JustifyCenter
tui.JustifyEnd
tui.JustifySpaceBetween
tui.JustifySpaceAround
tui.JustifySpaceEvenly
// tui.Align
tui.AlignStart
tui.AlignCenter
tui.AlignEnd
tui.AlignStretch
// tui.TextAlign
tui.TextAlignLeft // default
tui.TextAlignCenter
tui.TextAlignRight
// tui.ScrollMode
tui.ScrollNone // default
tui.ScrollVertical
tui.ScrollHorizontal
tui.ScrollBoth
// tui.OverflowMode
tui.OverflowVisible // default
tui.OverflowHidden
// tui.Color - terminal colors
tui.Black, tui.Red, tui.Green, tui.Yellow, tui.Blue, tui.Magenta, tui.Cyan, tui.White
tui.BrightBlack, tui.BrightRed, tui.BrightGreen, tui.BrightYellow, tui.BrightBlue, tui.BrightMagenta, tui.BrightCyan, tui.BrightWhite
tui.ANSIColor(index) // ANSI 256 palette
tui.RGBColor(r, g, b) // 24-bit true color
tui.HexColor("#ff6600") // Hex string to Color
tui.DefaultColor() // Terminal default
// tui.Gradient - color gradients
tui.NewGradient(tui.Red, tui.Blue) // Horizontal gradient
tui.NewGradient(tui.Red, tui.Blue).WithDirection(tui.GradientVertical) // Vertical
// Directions: GradientHorizontal, GradientVertical, GradientDiagonalDown, GradientDiagonalUp
// tui.Style - text styling (chainable)
tui.NewStyle().Bold().Foreground(tui.Red)
tui.NewStyle().Dim().Italic().Underline().Background(tui.Blue)
// Methods: Bold(), Dim(), Italic(), Underline(), Blink(), Reverse(), Strikethrough(), Foreground(Color), Background(Color)
// tui.State[T] - reactive state
count := tui.NewState(0)
count.Set(count.Get() + 1)
count.Update(func(v int) int { return v + 1 }) // atomic read-modify-write
count.Bind(func(v int) { /* called on change */ })
// tui.Ref - element references for hit testing
ref := tui.NewRef() // Single element ref
list := tui.NewRefList() // Multiple elements from loops
m := tui.NewRefMap[string]() // Keyed element refs
// tui.Events[T] - cross-component event bus
bus := tui.NewEvents[MyEvent]("topic-name")
bus.Emit(MyEvent{...})
unsub := bus.Subscribe(func(e MyEvent) { ... })
// tui.AnyKey - matches any key event (special or printable)
// Used by modal to block parent component key handlers
var AnyKey KeyMatcher
// Preemptive key bindings
tui.OnPreemptStop(matcher, handler) // Fires before normal handlers, stops propagation
Component Interfaces
Components are structs implementing Component (requires Render(app *App) *Element).
Additional optional interfaces add capabilities:
| Interface | Method | Description |
|---|---|---|
Component |
Render(app *App) *Element |
Required. Returns the element tree. |
KeyListener |
KeyMap() KeyMap |
Keyboard input handling via key bindings |
MouseListener |
HandleMouse(MouseEvent) bool |
Mouse input handling |
Initializer |
Init() func() |
Setup on mount; returned func is cleanup on unmount |
WatcherProvider |
Watchers() []Watcher |
Timers, tickers, channel watchers |
PropsUpdater |
UpdateProps(fresh Component) |
Receive updated props when re-rendered from cache |
AppBinder |
BindApp(app *App) |
Auto-called by mount system for State/Events fields |
AppUnbinder |
UnbindApp() |
Detach app-bound resources on unmount |
Event Handling
Key Bindings (KeyMap)
Implement KeyListener to handle keyboard input:
func (c *myComponent) KeyMap() tui.KeyMap {
return tui.KeyMap{
tui.On(tui.KeyEnter, c.onEnter), // Specific key, broadcast
tui.OnStop(tui.KeyEscape, c.onEscape), // Specific key, stop propagation
tui.On(tui.Rune('q'), func(ke tui.KeyEvent) { ... }), // Specific character
tui.OnStop(tui.AnyRune, c.onTyping), // All printable chars, exclusive
tui.OnPreemptStop(tui.AnyKey, c.onBlock), // Preemptive, stops propagation
}
}
The OnPreemptStop variant fires in a preemptive pass before all normal handlers. Used by the Modal component to block parent key handlers when the modal is open.
When a hand-written component hosts more than one focusable widget (say a TextArea next to an Input), mount each with app.Mount instead of merging their KeyMap()s onto the host. OnFocused only gates on focus when the binding's owning component implements IsFocused() and is mounted, so merging keymaps onto a host without IsFocused() drops the gate and the first widget swallows every key; the dispatch builder now errors when it detects this.
Mouse Click Handling
Implement MouseListener with ref-based hit testing:
func (c *counter) HandleMouse(me tui.MouseEvent) bool {
return tui.HandleClicks(me,
tui.Click(c.incrementBtn, c.increment),
tui.Click(c.decrementBtn, c.decrement),
)
}
Component Watchers
Implement WatcherProvider for component-level timers/channels:
func (t *timer) Watchers() []tui.Watcher {
return []tui.Watcher{
tui.OnTimer(time.Second, t.tick),
tui.NewChannelWatcher(t.dataChan, t.onData),
}
}
Cross-Component Communication (Events)
Use Events[T] for pub/sub between components:
// In component struct
type myComponent struct {
notifications *tui.Events[Notification] // created with tui.NewEvents[Notification]("notifications")
}
// Emit from one component
c.notifications.Emit(Notification{Message: "hello"})
// Subscribe in another component
unsub := c.notifications.Subscribe(func(n Notification) { ... })
Layout System
The layout engine (internal/layout) implements CSS flexbox with:
RowandColumndirectionsJustifyContent: Start, Center, End, SpaceBetween, SpaceAround, SpaceEvenlyAlignItems: Start, Center, End, Stretch- Padding, margin, and gap
- Min/max width/height constraints
- Percentage, fixed, and auto values
- Float-precision positioning for jitter-free animation
Design Patterns
- Functional Options:
ElementandAppuseOption/AppOptionfuncs for configuration - Double Buffering:
Buffermaintains front/back grids with diff-based rendering - Dirty Flags: Global dirty flag triggers re-layout/re-render when state changes
- Reactive State:
State[T]withBind()callbacks andBatch()for coalescing - Interface-based:
Viewable,Layoutable,Focusable,Watcher,Terminal - Tree Walking: BFS traversal for component dispatch (key/mouse events, watcher collection); DFS for focus discovery and rendering
- Component Caching: Mount system with mark-and-sweep cleanup — components are cached by (parent, index) key and reused across renders
- Ref System: Type-safe element references (
Ref,RefList,RefMap[K]) for event handling and hit testing - Event Bus: Generic
Events[T]for topic-based pub/sub between components - Key Dispatch: Priority-ordered key binding dispatch by tree position via
KeyListener/KeyMap - Preemptive Dispatch: Modal key bindings fire before normal tree-order dispatch, blocking parent handlers when the overlay is open
Testing
Use table-driven tests for unit tests that cover multiple cases of the same
behavior, with the following pattern:
type tc struct {
// test case fields
}
tests := map[string]tc{
"test name": {
// test case values
},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
// test logic
})
}
Always define the tc struct separately before the test map.
Plain linear tests are fine when there is no case dimension: sequential
lifecycle or state-transition tests, goroutine-coordination tests, and
single-scenario assertion sets. Do not force these into a one-row table.
Running Tests
go test ./... # Run all tests
go test -race ./... # Run all tests with race detector
go test ./internal/tuigen/... # Run tuigen tests
go test ./internal/lsp/... # Run LSP tests
go test ./internal/layout/... # Run layout tests
go test ./internal/formatter/... # Run formatter tests
go test -run TestParser ./... # Run specific test
Building
go build -o tui ./cmd/tui # Build CLI
./tui generate ./examples/... # Generate example code