Skip to content

allisonhere/tideui

Repository files navigation

tideui

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.

tideui three-pane layout and theme picker

Lineage

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.

Install

go get github.com/allisonhere/tideui

Features

  • Five layout modesStackedRight, ThreeColumn, SidebarOnly, Tabbed, and Floating, 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 Row and multi-line Block with selected/muted states.
  • Per-pane scrolling via Pane.ScrollOffset and the PaneScroller helper.
  • 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.

Quick start

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"},
})

Bubble Tea integration

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(),
    })
}

Layout modes

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.

Theming

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.

Rows and blocks

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).

Scrollable panes

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.

Theme picker

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.Modal

The picker previews live as you navigate and restores the confirmed theme on cancel.

Terminal background

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)

Design

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.

License

MIT.