A themeable, multi-pane terminal UI toolkit for Bubble Tea and Lipgloss.
tideui renders application-provided content inside themed pane shells, status
bars, and overlays. It is deliberately view-oriented: your application keeps
its own Bubble Tea model, key routing, persistence, and viewport state — you
hand tideui strings and dimensions, and it returns a framed, themed view.
The three-pane layout, theme-preview workflow, and themed modal language began
in Tide, a terminal RSS reader, and were
refined in TideMail, a keyboard-first
email client. tideui packages those reusable primitives for any Bubble Tea
application.
go get github.com/allisonhere/tideui- Five layout modes —
StackedRight,ThreeColumn,SidebarOnly,Tabbed, andFloating, each with tunable ratios. - Nineteen built-in palettes (Catppuccin, Nord, Dracula, Gruvbox, and more) with per-field background/foreground/accent overrides.
- Themed chrome — pane headers, status bars, centered modal overlays, and a ready-made theme picker.
- List primitives — single-line
Rowand multi-lineBlockwith selected/muted states. - Per-pane scrolling via
Pane.ScrollOffsetand thePaneScrollerhelper. - Density + accessibility — compact/comfortable spacing and a VT52 ASCII mode.
- Bounded output — never exceeds the requested terminal dimensions, down to tiny windows.
- Terminal background control — exposes the escape sequences so the app, not the library, writes to the terminal.
import "github.com/allisonhere/tideui"
theme, _ := tideui.ThemeByName("catppuccin-mocha")
renderer := tideui.NewRenderer(theme, tideui.StyleOptions{Density: tideui.Compact})
view := renderer.Render(tideui.Layout{
Width: 80, Height: 24, Mode: tideui.StackedRight,
Panes: [3]tideui.Pane{
{Title: "Mailboxes", Content: "Inbox\nArchive", Focused: true},
{Title: "Messages", Content: "Welcome to tideui"},
{Title: "Preview", Content: "Application-owned content."},
},
Status: &tideui.StatusBar{Left: "ready", Right: "? help"},
})tideui owns no model state. Track dimensions and theme in your own model and
build a renderer in View:
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if size, ok := msg.(tea.WindowSizeMsg); ok {
m.width, m.height = size.Width, size.Height
}
return m, nil
}
func (m model) View() string {
r := tideui.NewRenderer(m.theme, tideui.StyleOptions{Density: m.density})
return r.Render(tideui.Layout{
Width: m.width, Height: m.height, Mode: tideui.ThreeColumn,
Panes: m.panes(),
})
}| Mode | Description | Ratio fields |
|---|---|---|
StackedRight |
Pane 0 sidebar; panes 1 & 2 stacked on the right | SidebarRatio, UpperRightRatio |
ThreeColumn |
All three panes side by side | ColumnRatios |
SidebarOnly |
Pane 0 sidebar; pane 1 full-height main (pane 2 unused) | SidebarRatio |
Tabbed |
Tab bar on top; the focused pane fills the area below | — |
Floating |
Pane 0 as background; panes 1 & 2 as floating panels | FloatWidthRatio, FloatHeightRatio |
layout.Mode = tideui.StackedRight
layout.SidebarRatio, layout.UpperRightRatio = 0.30, 0.45
layout.Mode, layout.ColumnRatios = tideui.ThreeColumn, [3]float64{2, 3, 5}In Tabbed mode the first Focused pane selects the active tab (falling back to
pane 0). All ratio fields default to sensible values when left zero.
theme, ok := tideui.ThemeByName("nord") // false if unknown
for _, t := range tideui.BuiltinThemes { /* ... */ }Override individual colors without forking a palette:
theme = tideui.ThemeOverrides{
Accent: "#f5c2e7",
}.Apply(tideui.CatppuccinMocha)Theme.UsesASCII() reports VT52 mode (ASCII-only glyphs) so callers can adapt.
StyleOptions{Density: tideui.Comfortable} adds spacing; Compact removes it.
RenderRow draws a single-line list item; RenderBlock adds an optional
multi-line body (a Block with no Body is byte-identical to the matching
Row). Both support Selected and Muted:
renderer.RenderRow(tideui.Row{Prefix: "* ", Text: "Inbox", Suffix: "12", Selected: true}, width)
renderer.RenderBlock(tideui.Block{
Prefix: "● ", Header: "alice", Meta: "10:02",
Body: "Multi-line body, indented to the header.",
}, width)For fully custom pane content, use the exported renderer.Styles (e.g.
DetailTitle, DetailMeta, DetailBody).
Set Pane.ScrollOffset, or let PaneScroller manage it:
m.scroll.ScrollDown(1) // ScrollUp / ScrollToTop also available
m.scroll.ClampTo(total, visible) // optional; renderer clamps out-of-range anyway
pane.ScrollOffset = m.scroll.Offset()CanScrollDown(total, visible) reports whether more content lies below.
A drop-in modal for previewing and confirming themes:
picker := tideui.NewThemePicker(tideui.ThemePickerOptions{InitialTheme: "nord"})
picker.Open("nord")
switch picker.Update(keyMsg) {
case tideui.ThemePickerConfirm:
m.theme = picker.ConfirmedTheme()
case tideui.ThemePickerCancel:
// preview reverted to the confirmed theme
}
overlay := picker.Modal(renderer, m.width, m.height) // assign to Layout.ModalThe picker previews live as you navigate and restores the confirmed theme on cancel.
tideui never writes to the terminal itself. To paint the terminal background
to match the theme, fetch the sequences and emit them from your program:
set, reset := tideui.TerminalBackgroundSequences(theme)Applications retain ownership of their model, commands, key routing,
persistence, and viewport state. tideui is purely presentational — it turns
content + dimensions + a theme into a bounded, framed string. The one exception
is the optional ThemePicker, which manages its own navigation once opened.
MIT.
