tuikit

package module
v0.8.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 25, 2026 License: MIT Imports: 22 Imported by: 0

README

tuikit

CI Security Go Reference Go Report Card

A small, reusable Bubble Tea frame kit: the structural chrome you rebuild in every terminal app — a numbered page wrapper with navigation, chip tabs, and bordered panels — decoupled from any one app and driven by a swappable theme.

Demos

From go run ./examples/demo (and ./examples/themed for the last one):

Chip sub-tabs (Tab cycles) Scrolling viewport
Panels Reader
SearchView, ActionRow, and Help Live theme switching (t)
Search Theme switch

Table, Pairs and the formatters, with a simulated transfer driving Meter and TransferredBytes. The STATE words are graded by StatusWord — green, amber, red — and cost the columns beside them nothing, because widths are measured by display width rather than by string length:

Table

Overlay, a modal popup composited over the page. p pins it to each of the nine anchors in turn and hjkl nudges it off one; the page underneath never reflows, and closing it leaves the page exactly as it was:

Overlay

Selection, from go run ./examples/selection. Capturing the mouse takes drag events away from the terminal, and its own text selection with them — so the app draws one. Dragging paints the range in place and releasing copies it, echoed in the footer:

Selection

StreamingMarkdown, from go run ./examples/streaming. A generated document arrives a character at a time; blocks that have settled are formatted and cached, and the unfinished tail is rendered too, with whatever constructs it leaves open closed synthetically. Watch a code fence arrive already chroma-highlighted, and no ** or backticks ever reach the screen — the answer never sits unformatted waiting for the stream to end:

Streaming markdown

The same demo's -reveal flag runs a rejected alternative: settled blocks played out on a display clock so nothing unfinished is ever drawn, paced by an adaptive playout buffer that sizes itself from the observed gap between blocks. It removes the reflow and still freezes inside a long fence, which is why it stayed in the demo — measurements and the reasoning are in docs/notes/streaming-reveal.md.

DiffView, from go run ./examples/diffview. A file edit rendered unified or side-by-side, line-numbered on both sides, highlighted by chroma, and — this is the part a plain +/- diff leaves to the reader — with the changed words inside a modified line pair picked out, so a renamed identifier is visible rather than hidden in an otherwise identical line. tab switches layouts; below 100 columns the side-by-side layout falls back to unified rather than truncating both halves. Diffing and highlighting are memoized per width and layout, since a static diff should not be recomputed every frame.

DiffView MemoList, from go run ./examples/memolist. A 5,000-message transcript in a 20-line viewport: only the visible window is rendered, each message's block is memoized by ID and revision, and the tail is followed until you scroll up. Watch the counter in the footer — "rendered this frame" stays in single digits through paging and jumps to the top, and while only the streaming tail changes it is 1, not the length of the scrollback:

MemoList

Fenced code is highlighted by chroma, and which stylesheet it uses is a choice — markdown.SyntaxThemes lists the 64 that ship, so an app can offer them. From go run ./examples/syntax, cycling three of them with tab. The light one is in the cycle to show what a stylesheet mismatched to the terminal looks like:

Syntax stylesheets

Switching means building a new render function — markdown.New captures the stylesheet — and a new StreamingMarkdown, whose cache is keyed on source and width alone and would otherwise keep serving the previous palette for blocks whose source never changes again.

Static screenshots
Panels Search
Panels page Search page
Reader About
Reader page About page
Widgets (Meter · Status · text helpers) Table (Table · Pairs · StatusWord)
Widgets page Table page
Overlay (modal popup, pinned right) Streaming markdown (boundary shown)
Overlay page Streaming page
Syntax stylesheets (catppuccin-mocha)
Syntax page

Components

  • Frame — a stateful tea.Model that hosts a list of pages, renders a numbered header ([1] Foo [2] Bar …), delegates the body to the active page, and draws a status footer. Number keys 19 switch pages; Ctrl+C quits.
  • Page — the seam you implement, a plain 3-method interface:
    type Page interface {
        Title() string
        Update(msg tea.Msg) tea.Cmd
        View(width, height int) string
    }
    
    Size is passed into View, so pages never track their own dimensions. Optionally implement InputCapturer so the Frame stops treating number keys as navigation while a field is focused.
  • Theme — the palette every component draws from. DefaultTheme() or roll your own and pass WithTheme.
  • TabStrip — a row of active/inactive chip tabs for sub-navigation within a page.
  • Panel / PanelStyle — bordered panels with a focused state.
  • SearchView — a scrollable text pane with an incremental substring filter and follow-to-bottom behavior: feed it lines, it renders the matching subset, stays pinned to the bottom as new lines arrive (until you scroll up), and toggles a search input on /. Matching is against each line's visible text (ANSI styling is stripped first), so colored lines still search cleanly. The log/reader viewport every terminal app rebuilds by hand.
  • StreamingMarkdown — markdown rendered while it is still arriving. Blocks that have provably settled are formatted and cached; the unfinished tail is redrawn each frame with its open constructs closed synthetically, so a partial code fence streams highlighted and no raw markers are ever visible, instead of the whole answer popping into shape at the end. WithRawTail opts out. Takes a RenderFunc; tuikit/markdown provides one backed by glamour, in a separate package so importing tuikit does not pull in a markdown engine. Fenced code is highlighted by chroma; markdown.WithSyntaxTheme picks the stylesheet.
  • Selection — a mouse drag over a rendered frame. It holds two Cells and nothing else: the frame is handed to Paint and Text on every call, so a selection cannot go stale against content re-rendered underneath it. Paint highlights the range in place and keeps every line's display width; Text returns what a copy gesture should put on the clipboard. Columns are display columns throughout, so wide runes and styled cells land where they look like they land.
  • Overlay — a dismissable floating panel: a titled, bordered, scrollable box drawn over the host's view rather than stacked above or below it, for the reference output (a help table, a usage report) that otherwise has nowhere to go but the main content stream. Render returns a frame of exactly the size it was given, so adopting one changes no layout math; Align pins it to any of nine anchors with a cell-wise nudge; Help adds host bindings to the hint row, which is always drawn — a modal with no visible way out is a trap.
  • ActionRow — a labelled row of selectable actions (Actions: Start [Stop] Restart); the selected action is bracketed and highlighted when the row is focused, muted otherwise.
  • Help / HelpLine — a bubbles/help model with brighter key and description colors than the dim bubbles default, plus a one-line short-help renderer.
  • Meter — a fixed-width horizontal gauge (filled/empty bar, no percentage label) over bubbles/progress, clamped to 0–100. The CPU/RAM/disk dial every dashboard needs.
  • Status — the "press again to confirm" destructive-action flow bundled with the success/error message it leaves behind: Confirm arms then fires, SetResult records the outcome, AppendRows renders it in the theme's colors.
  • Table / Pairs — a header and [][]string body in, an aligned block out: widths measured over the header too, header upper-cased and muted. Pairs is the one-dimensional case, for key/value data. Both are built on Columns, JoinCells, Pad and Widest, which stay exported for layouts they do not cover. All measure with ansi.StringWidth, so a styled cell aligns like its plain equivalent instead of padding by the length of its escape sequence.
  • PainterPainterFor(w) decides once whether output may carry escapes (w is a terminal, NO_COLOR unset) and returns Paint or Plain, so no call site repeats that test — or gets it subtly different, which is how half a command's output ends up coloured. Plain also keeps test assertions readable.
  • ClassifyStatus / StatusWord — grade a status word into a Level and paint it: green for a healthy state, red for a failure, amber for anything unfamiliar, so an operator only reads the words that are not green.
  • Layout & text helpersTitleize (running_profilesRunning profiles), StatusTitle, Field, Rule, VerticalSlice, Flow, AdaptiveWidth (responsive column width), Indent/IndentLines, TruncMiddle (rune-aware middle-ellipsis), FormatBytes (IEC sizes), TransferredBytes ("4.1 GiB / 6.6 GiB"), CoarseDuration (three significant figures), Age ("3m0s ago"), and EmptyPanel (placeholder).

Usage

frame := tuikit.New(
    tuikit.WithBrand("myapp", "does a thing"),
    tuikit.WithPages(newHomePage(), newSettingsPage()),
    tuikit.WithStatus(func() (string, tuikit.Level) { return "Ready", tuikit.LevelInfo }),
)
tea.NewProgram(frame).Run()

Docs

  • docs/examples.md — copy-paste snippets for every component.
  • scripts/record.py — regenerates the gif and screenshot above.
  • Package overview / API reference: go doc github.com/antonikliment/tuikit.

Demo

go run ./examples/demo    # pages, tabs, reader, SearchView, ActionRow, Help, Meter/Status, Table/Pairs, Overlay
go run ./examples/streaming -seed=7 -cps=80 -block=3 -debug   # StreamingMarkdown under an endless generated stream
go run ./examples/themed  # live theme switching — press t to cycle palettes
go run ./examples/syntax  # chroma stylesheets for fenced code — press tab to cycle
go run ./examples/selection # drag to select and copy, painted by the app

Number keys switch pages; on the Panels page Tab switches sub-panels; on the Search page / focuses the field (and digits then type instead of navigating); on the Table page any key starts the simulated transfer; on the Overlay page p pins the popup to the next anchor and hjkl nudges it.

License

MIT

Documentation

Overview

Package tuikit is a small, reusable Bubble Tea frame kit: the structural chrome you rebuild in every terminal app — a numbered page wrapper with navigation, chip tabs, and bordered panels — decoupled from any one app and driven by a swappable Theme.

Frame and pages

A Frame is a tea.Model that hosts a slice of pages, renders a numbered header ("[1] Foo [2] Bar …"), delegates the body to the active page, and draws a status footer. Number keys 1-9 switch pages; Ctrl+C quits.

A page is any value implementing Page, a plain three-method interface:

type Page interface {
	Title() string
	Update(msg tea.Msg) tea.Cmd
	View(width, height int) string
}

Size is passed into View, so a page never tracks its own dimensions. Implement the page on a pointer type so Update can mutate state. A page may additionally implement InputCapturer so the Frame stops treating number keys as navigation while a field is focused.

Building blocks

Pages assemble their bodies from the theme-driven helpers:

See the docs/examples.md file for copy-paste snippets, and examples/demo for a runnable showcase.

Index

Constants

View Source
const AgeCutoff = 24 * time.Hour

AgeCutoff is how old an instant may be before Age stops rendering it as an elapsed time. Past a day "27h14m ago" is harder to place than the timestamp it came from.

View Source
const DefaultDiffSyntaxTheme = "tokyonight-night"

DefaultDiffSyntaxTheme is the chroma stylesheet ChromaHighlighter uses when given an empty name, matching the markdown package's default so a program using both does not show two palettes.

View Source
const IndentWidth = 2

IndentWidth is how many spaces Indent adds per level. Wide enough to see, narrow enough that a nested block still fits an 80-column terminal.

View Source
const SplitMinWidth = 100

SplitMinWidth is the narrowest total width DiffView.RenderSplit will lay two columns out in. Below it each half is under 50 columns, which turns every line of real code into a truncation, so RenderSplit falls back to the unified layout instead of producing something unreadable.

View Source
const TabWidth = 4

TabWidth is how many spaces DiffView expands a leading or embedded tab to.

Variables

This section is empty.

Functions

func AdaptiveWidth added in v0.2.2

func AdaptiveWidth(total, gap, minimum, maximum int) int

AdaptiveWidth computes a responsive column width: it fits as many columns of at least min width (separated by gap) as total allows, then divides the space evenly, clamping each column to [min, max].

func Age added in v0.3.0

func Age(at, now time.Time) string

Age renders at as how long before now it was: "3h23m0s ago". An operator reading a log or an event list wants "how long ago", and computing that from an RFC 3339 string in their head is the work this saves.

Outside the window — an instant in the future, or older than AgeCutoff — the elapsed form stops being the useful one and the absolute local time comes back as RFC 3339. now is a parameter rather than a call to time.Now so the result is a pure function of its inputs and a test need not sleep.

func CoarseDuration added in v0.3.0

func CoarseDuration(d time.Duration) time.Duration

CoarseDuration rounds d to roughly three significant figures, scaling the unit with the magnitude: 272.914939ms becomes 272ms while 1.234µs keeps its precision.

Nanosecond precision on a wall-clock duration is noise in a column an operator is scanning for the one slow entry — 272.914939ms and 272ms lead to the same conclusion, and only one of them lines up. Durations under a microsecond are returned unchanged, as is the zero value. Negative durations round by magnitude and keep their sign.

func ColorEnabled added in v0.3.1

func ColorEnabled(w io.Writer) bool

ColorEnabled reports whether output to w may carry ANSI escapes: w is a terminal and NO_COLOR is unset.

It is a separate question from IsTerminalWriter on purpose. NO_COLOR says "do not paint", not "change format": a user who sets it still wants the readable table, just without the escapes. Conflating the two hands them the machine format, which is the opposite of what they asked for. See https://no-color.org.

func Columns added in v0.3.0

func Columns(rows [][]string) []int

Columns measures each column of rows by its widest cell, returning one width per column. Short rows are allowed: the result is as long as the longest row, and a column no row reaches simply does not appear.

Width is measured with ansi.StringWidth rather than len, because a styled cell carries escape sequences that occupy no screen columns and East Asian characters occupy two. Measuring with len pads by the length of the escape sequence and skews the whole table right — the bug every hand-rolled terminal table hits once.

Pass the header row along with the body so a column never renders narrower than its own title:

widths := tuikit.Columns(append([][]string{header}, rows...))

func Field

func Field(label, value string) string

Field renders an aligned "label: value" pair.

func Flow

func Flow(width, gap int, blocks []string) string

Flow lays blocks out left-to-right, wrapping to a new row when the next block would overflow width, separated by gap spaces.

func FormatBytes added in v0.2.2

func FormatBytes(size int64) string

FormatBytes renders a byte count as a human-readable IEC size (B, KiB, MiB, GiB, TiB, PiB) with one decimal place. It carries no external dependency so tuikit stays dependency-light.

func Help added in v0.2.0

func Help() help.Model

Help returns a bubbles/help model with brighter key and description colors than the bubbles default, which renders very dim on many terminals. Use it directly for a full multi-column help layout, or call HelpLine for the common single-line short help.

func HelpLine added in v0.2.0

func HelpLine(bindings ...key.Binding) string

HelpLine renders bindings as a single "key desc • key desc" short-help line using the brightened styles.

func Indent added in v0.3.0

func Indent(text string, depth int) string

Indent prefixes text with depth levels of IndentWidth spaces. A depth of zero or less returns text unchanged.

func IndentLines added in v0.3.0

func IndentLines(text string, depth int) string

IndentLines applies Indent to every line of text and returns the result with a trailing newline. Trailing blank lines in the input are dropped first, so indenting an already-newline-terminated block does not accumulate them. Empty input yields an empty string rather than a lone newline.

func IsTerminalWriter added in v0.3.1

func IsTerminalWriter(w io.Writer) bool

IsTerminalWriter reports whether w is a terminal a human is watching. It is the predicate that decides human-vs-machine output.

It asks about the output stream alone: rendering only writes, so a program reading nothing from stdin still has a human reading its stdout.

func JoinCells added in v0.3.0

func JoinCells(row []string, widths []int, gap int) string

JoinCells lays one row out against widths, separating columns by gap spaces. A negative gap is treated as zero.

The final cell is not padded and trailing whitespace is trimmed, so no line carries invisible padding into a terminal's selection or a diff. Cells beyond the end of widths are joined at their natural size, so a row longer than the measured table still renders every cell rather than dropping it.

func Pad added in v0.3.0

func Pad(text string, width int) string

Pad right-pads text with spaces to width, measuring as Columns does. Text already at or beyond width is returned untouched — Pad never truncates, so a value is never silently cut; reach for TruncMiddle first when a hard cap is what you want.

func Paint added in v0.3.1

func Paint(s lipgloss.Style, text string) string

Paint is the Painter that always paints, for output already known to be going to a terminal.

func Plain added in v0.3.1

func Plain(_ lipgloss.Style, text string) string

Plain is the Painter that never paints. Tests use it to assert on content without decoding escape sequences.

func SegmentBar added in v0.8.0

func SegmentBar(width int, segments []Segment, free Segment) string

SegmentBar renders a width-cell horizontal bar split proportionally by the segments' shares, with a swatch legend of label/value/percent rows beneath. Every segment with a positive share paints at least one cell so tiny slices stay visible; free absorbs whatever the segments leave of the bar and closes the legend as the "□" row.

func SetTheme

func SetTheme(theme Theme) tea.Cmd

SetTheme returns a command that re-themes the Frame. Pages that want to follow suit should share a *Theme the app mutates alongside sending this.

func Titleize added in v0.3.1

func Titleize(name string) string

Titleize turns an identifier into a label: "running_profiles" and its camel spelling "runningProfiles" both become "Running profiles". Sentence case, not Title Case, so "Autostart profiles" does not read as a proper noun.

func TransferredBytes added in v0.3.0

func TransferredBytes(received, total int64) string

TransferredBytes renders transfer progress as "4.1 GiB / 6.6 GiB" using FormatBytes.

A total of zero or less means "not yet known" — a multi-file download does not learn its size until every manifest is fetched — and renders as the received count alone. That is deliberate: "4.1 GiB / 0 B" reads as a bug, and a caller with no total should show a byte counter rather than a progress bar pinned at zero.

func TruncMiddle added in v0.2.2

func TruncMiddle(s string, width int) string

TruncMiddle keeps a string on a single line by eliding its middle with an ellipsis once it exceeds width runes, so long paths never orphan onto a wrapped line beneath their label. It is rune-aware; width counts runes.

func VerticalSlice

func VerticalSlice(content string, offset, height int) string

VerticalSlice hard-clips content to height lines starting at offset, so a block never grows past a fixed footprint.

func Widest added in v0.3.0

func Widest(values []string) int

Widest returns the display width of the widest value, or 0 for an empty slice. It is the one-dimensional Columns: the width to Pad a label column to when the rows are key/value pairs rather than a table.

Types

type Alignment added in v0.5.0

type Alignment struct {
	Horizontal lipgloss.Position // lipgloss.Left, Center, Right
	Vertical   lipgloss.Position // lipgloss.Top, Center, Bottom
	ShiftX     int
	ShiftY     int
}

Alignment pins an Overlay in its frame: an edge (or center) on each axis, plus a nudge in cells. Positive Shift moves right and down. The box is kept inside the frame whatever the shift asks for.

type Cell added in v0.7.0

type Cell struct{ Row, Col int }

Cell is a position in a rendered frame: Row counts lines from the top, Col counts display columns from the left. It is the coordinate a mouse event arrives in, which is why nothing here knows about viewports or scroll offsets — the caller hands over the frame it just drew.

type DiffView added in v0.8.1

type DiffView struct {
	// contains filtered or unexported fields
}

DiffView renders a diff between two versions of a file, unified or side-by-side, syntax-highlighted and with the changed spans inside a modified line pair picked out.

A raw +/- diff is the hardest artifact a TUI produces to read: no color, no intraline emphasis, and a renamed identifier hides in a wall of otherwise identical text. DiffView is the readable form of the same data, driven by a Theme like the rest of the kit.

It renders a string and holds no scroll state — the caller owns the viewport — and it does no file I/O; content is passed in.

out := NewDiffView(theme).
	Before("main.go", old).
	After("main.go", new).
	ContextLines(3).
	Render(width)

Diffing and highlighting are O(file) and a TUI repaints every frame, so results are memoized per (width, layout) and the memo is dropped on any builder mutation. A DiffView is therefore not safe for concurrent use.

func NewDiffView added in v0.8.1

func NewDiffView(theme Theme) *DiffView

NewDiffView returns a DiffView drawing from theme, painting unconditionally (Paint) and highlighting with ChromaHighlighter, with three lines of context and no line cap.

func (*DiffView) After added in v0.8.1

func (d *DiffView) After(path, content string) *DiffView

After sets the new path and content.

func (*DiffView) Before added in v0.8.1

func (d *DiffView) Before(path, content string) *DiffView

Before sets the original path and content.

func (*DiffView) ContextLines added in v0.8.1

func (d *DiffView) ContextLines(n int) *DiffView

ContextLines sets how many unchanged lines are kept either side of a change. A negative count means zero; a count at or above the file length shows the whole file as one hunk.

func (*DiffView) Highlighter added in v0.8.1

func (d *DiffView) Highlighter(h Highlighter) *DiffView

Highlighter sets the syntax highlighter. A nil highlighter disables highlighting, which is what tests and plain-text destinations want.

func (*DiffView) MaxLines added in v0.8.1

func (d *DiffView) MaxLines(n int) *DiffView

MaxLines caps the output at n rendered lines, replacing the remainder with a "… +N more lines" tail so a caller can show a preview and expand later. Zero or less means no cap.

Like every setter, an unchanged value is a no-op, so a view function may set it every frame without dropping the render memo.

func (*DiffView) Painter added in v0.8.1

func (d *DiffView) Painter(p Painter) *DiffView

Painter sets how styles are applied — pass Plain to render without escapes.

func (*DiffView) Render added in v0.8.1

func (d *DiffView) Render(width int) string

Render lays the diff out as a unified diff at width.

func (*DiffView) RenderSplit added in v0.8.1

func (d *DiffView) RenderSplit(width int) string

RenderSplit lays the diff out side by side: old on the left, new on the right, aligned row by row. Below SplitMinWidth it renders unified instead.

type Frame

type Frame struct {
	// contains filtered or unexported fields
}

Frame is the stateful page wrapper: a numbered header, a body delegated to the active Page, and a status footer. It implements tea.Model, so it can be handed straight to tea.NewProgram.

func New

func New(opts ...Option) *Frame

New builds a Frame. Defaults: DefaultTheme, DefaultKeyMap, 120x32 until the first WindowSizeMsg.

func (*Frame) ActivePage

func (f *Frame) ActivePage() int

ActivePage returns the current page index.

func (*Frame) Init

func (f *Frame) Init() tea.Cmd

Init implements tea.Model.

func (*Frame) SetTheme

func (f *Frame) SetTheme(theme Theme)

SetTheme re-themes the Frame's chrome immediately.

func (*Frame) Theme

func (f *Frame) Theme() Theme

Theme returns the frame's theme, handy for pages that want to match it.

func (*Frame) Update

func (f *Frame) Update(msg tea.Msg) (tea.Model, tea.Cmd)

Update implements tea.Model.

func (*Frame) View

func (f *Frame) View() tea.View

View implements tea.Model.

type GlobalKeyFunc

type GlobalKeyFunc func(msg tea.KeyPressMsg) (cmd tea.Cmd, handled bool)

GlobalKeyFunc handles app-wide keys before they reach the active page (but after Quit, and only when the active page is not capturing input). Return handled=true to consume the key; the returned command, if any, is run.

type Highlighter added in v0.8.1

type Highlighter func(filename, code string) string

Highlighter colors one line of code for display, keyed by the filename it came from. It must never fail: a line it cannot lex is returned as it came in, because an unhighlightable file must still be readable.

Its result carries the highlighter's own SGR sequences — see RenderFunc for why that means not wrapping it in a foreground style.

func ChromaHighlighter added in v0.8.1

func ChromaHighlighter(styleName string) Highlighter

ChromaHighlighter returns a Highlighter that colors a line with chroma, picking the lexer from the filename. An unknown stylesheet or an unlexable file degrades to plain text rather than to an error.

Lines are lexed one at a time, since a diff shows lines out of order and a hunk has no whole-file context to lex against. The cost is that a line inside a multi-line string or comment is colored as if it were code — visible, but far cheaper than re-lexing both files for every frame.

The returned function caches its lexer per filename and is safe for concurrent use.

type InputCapturer

type InputCapturer interface {
	CapturingInput() bool
}

InputCapturer is an optional Page capability: while CapturingInput reports true (e.g. a focused search field), the Frame stops treating number keys as page navigation and forwards them to the page instead.

type KeyMap

type KeyMap struct {
	// PageDigits documents the number-key navigation for help output; the Frame
	// matches digits 1-9 directly regardless of this binding's keys.
	PageDigits key.Binding
	Quit       key.Binding
}

KeyMap holds the Frame-level navigation bindings. Page switching is by number key so that Tab/Shift+Tab stay free for pages to use internally (e.g. panel focus). Override via WithKeyMap.

func DefaultKeyMap

func DefaultKeyMap() KeyMap

DefaultKeyMap returns the stock bindings: 1-9 switch pages, Ctrl+C quits.

type Level

type Level int

Level classifies footer status text.

const (
	LevelInfo Level = iota
	LevelSuccess
	LevelWarning
	LevelError
)

Footer status levels, used to color the status line.

func ClassifyStatus added in v0.3.1

func ClassifyStatus(word string) Level

ClassifyStatus grades a status word so it can be colored without every caller keeping its own list of what "fine" looks like. Matching is case-insensitive; anything unrecognized is LevelWarning, which is the safe default — an unfamiliar state is one an operator should read.

An empty string is LevelInfo: there is nothing to grade.

type ListItem added in v0.8.1

type ListItem interface {
	// ID identifies the item across frames. It is the memo key, so two items
	// sharing an ID share a cache entry: give every item its own.
	ID() string
	// Render lays the item out at width. It is called only when the item is
	// about to be measured or drawn and its cache entry is missing.
	Render(width int) string
}

ListItem is one block of a MemoList: a stable identity and a full render at a given width. Render may return many lines — a chat message, a tool block, a diff — and the list treats it as one indivisible unit for caching, though it will show a partial one at the edges of the viewport.

type MemoList added in v0.8.1

type MemoList struct {
	// contains filtered or unexported fields
}

MemoList is a virtualized list for long scrollback — a chat transcript, a log pane — that renders only what is on screen and memoizes each item's rendered block. A view that rebuilds its whole content every frame costs O(transcript) per repaint, which is why long agent sessions in terminal chat UIs get slower as they get longer; here scrolling is a walk over cached lines and appending costs one item's render.

The list owns no colors and no chrome: items render themselves, so a caller composes a Panel or a footer around the result. It also owns no input — pass scroll deltas to MemoList.ScrollBy from whatever keys or wheel events the host binds, keeping it free of any framework. It runs no goroutines.

The zero value is not usable; build one with NewMemoList.

func NewMemoList added in v0.8.1

func NewMemoList() MemoList

NewMemoList returns an empty MemoList following the tail.

func (*MemoList) Append added in v0.8.1

func (l *MemoList) Append(items ...ListItem)

Append adds items at the tail — the common case, and the cheap one: nothing already cached is touched, so a frame that appends one message renders one message.

func (*MemoList) Following added in v0.8.1

func (l *MemoList) Following() bool

Following reports whether the list is stuck to the tail.

func (*MemoList) Invalidate added in v0.8.1

func (l *MemoList) Invalidate(id string)

Invalidate drops one item's cached render, so the next frame rebuilds exactly that item. This is the streaming case: the last item grows every frame and nothing above it does.

func (*MemoList) InvalidateAll added in v0.8.1

func (l *MemoList) InvalidateAll()

InvalidateAll drops every cached render — for a theme swap, or any change to how items draw that the items themselves do not report.

func (*MemoList) Len added in v0.8.1

func (l *MemoList) Len() int

Len is the number of items.

func (*MemoList) Render added in v0.8.1

func (l *MemoList) Render(width, height int) string

Render lays out the visible window: width columns, at most height lines. Items outside the window are never rendered, and items inside it are rendered only on a cache miss, so a steady frame over a settled transcript does no work beyond joining cached lines.

A width change invalidates everything, since wrapping changes every height. An empty list renders "", and a viewport taller than the content renders just the content.

func (*MemoList) ScrollBy added in v0.8.1

func (l *MemoList) ScrollBy(delta int)

ScrollBy moves the view by delta lines: negative up, positive down. Scrolling up stops following the tail, and scrolling down resumes it on arrival at the bottom — the universal chat behavior.

It is a no-op before the first MemoList.Render, which is where the viewport size comes from.

func (*MemoList) ScrollToBottom added in v0.8.1

func (l *MemoList) ScrollToBottom()

ScrollToBottom returns to the tail and resumes following it.

func (*MemoList) ScrollToTop added in v0.8.1

func (l *MemoList) ScrollToTop()

ScrollToTop jumps to the first item and stops following.

func (*MemoList) SetItems added in v0.8.1

func (l *MemoList) SetItems(items []ListItem)

SetItems replaces the backing items. Cache entries survive by ID, so reordering, prepending history, or replacing the slice with a superset of itself costs no renders; entries for items that have gone away are dropped once enough of them accumulate.

type Meter added in v0.2.2

type Meter struct {
	// contains filtered or unexported fields
}

Meter is a fixed-width horizontal gauge (a filled/empty bar with no percentage label) over bubbles/progress. Use it for resource dials like CPU or memory.

func NewMeter added in v0.2.2

func NewMeter(width int, fill color.Color) Meter

NewMeter builds a Meter width cells wide, filled in the given color.

func (Meter) View added in v0.2.2

func (g Meter) View(percent int) string

View renders the meter at percent, clamped to 0..100.

type Option

type Option func(*Frame)

Option configures a Frame.

func WithBrand

func WithBrand(brand, tagline string) Option

WithBrand sets the header brand name and an optional tagline beside it.

func WithGlobalKeys

func WithGlobalKeys(fn GlobalKeyFunc) Option

WithGlobalKeys registers an app-wide key handler (theme toggle, help, etc.).

func WithKeyMap

func WithKeyMap(k KeyMap) Option

WithKeyMap overrides the navigation bindings.

func WithPages

func WithPages(pages ...Page) Option

WithPages sets the pages, in tab order.

func WithStatus

func WithStatus(status StatusFunc) Option

WithStatus sets the footer status provider.

func WithTheme

func WithTheme(t Theme) Option

WithTheme overrides the default palette.

type Overlay added in v0.5.0

type Overlay struct {
	// Theme supplies the border, title, and hint colors.
	Theme Theme
	// Accent is the border color. Zero means the theme's Brand.
	Accent color.Color
	// Help is extra bindings to advertise in the hint row, after the built-in
	// close and scroll keys. The row is always drawn, so these cost no height;
	// acting on them is the host's job (see Update).
	Help []key.Binding
	// Align pins the box in the frame. NewOverlay centers it.
	Align Alignment
	// contains filtered or unexported fields
}

Overlay is a dismissable floating panel: a titled, bordered, scrollable box drawn *over* a host's view rather than stacked above or below it. It is the place for reference output — a help table, a usage report, a tool list — that a host would otherwise have to splice into its main content and later scroll away.

A host keeps one Overlay, calls Open when a view is requested, offers keys to Update before its own handling, and wraps its finished view in Render. While closed, Update claims nothing and Render returns the background untouched, so an Overlay costs a host two lines and changes no existing behavior.

func NewOverlay added in v0.5.0

func NewOverlay(theme Theme) Overlay

NewOverlay returns a closed Overlay drawing from the given theme, centered.

func (*Overlay) Close added in v0.5.0

func (o *Overlay) Close()

Close hides the Overlay and drops its content.

func (*Overlay) Content added in v0.5.0

func (o *Overlay) Content() string

Content is the text the Overlay was opened with, unwrapped.

func (*Overlay) IsOpen added in v0.5.0

func (o *Overlay) IsOpen() bool

IsOpen reports whether the Overlay is showing.

func (*Overlay) Open added in v0.5.0

func (o *Overlay) Open(title, content string)

Open shows content under the given title, scrolled to the top. Calling it on an already-open Overlay replaces what is showing.

func (*Overlay) Render added in v0.5.0

func (o *Overlay) Render(bg string, width, height int) string

Render composites the Overlay over bg, centered in a width×height frame. The result is exactly that size, so a host can swap it in for its own view without redoing any height math. A closed Overlay renders as bg.

func (*Overlay) Title added in v0.5.0

func (o *Overlay) Title() string

Title is the title the Overlay was opened with.

func (*Overlay) Update added in v0.5.0

func (o *Overlay) Update(msg tea.Msg) bool

Update offers a message to an open Overlay and reports whether it was claimed. Esc and "q" close it; the arrow/page/home/end keys scroll. A closed Overlay claims nothing, so a host can put this first in its key chain.

type Page

type Page interface {
	// Title is the tab label shown in the numbered header.
	Title() string
	// Update handles a message addressed to this page (it is the active page).
	Update(msg tea.Msg) tea.Cmd
	// View renders the page body into the given content area.
	View(width, height int) string
}

Page is a single screen hosted by a Frame. Implement it on a pointer type so Update can mutate state. Size is passed into View, so a page never tracks its own width/height.

type Painter added in v0.3.1

type Painter func(lipgloss.Style, string) string

Painter applies a style to text, or returns it untouched when the destination cannot take escapes.

Passing one of these around is what stops every call site from repeating the "is this a terminal, and is NO_COLOR set" test — and from getting it subtly different, which is how half a command's output ends up colored.

func PainterFor added in v0.3.1

func PainterFor(w io.Writer) Painter

PainterFor returns the Painter appropriate to w, deciding once so the answer cannot change halfway through rendering one command's output.

type Panel

type Panel struct {
	Theme   Theme
	Accent  color.Color
	Focused bool
	Width   int
	Height  int
}

Panel is a convenience wrapper around PanelStyle with size baked in. Zero Width/Height means "fit content".

func (Panel) Render

func (p Panel) Render(content string) string

Render draws content inside the panel.

type RenderFunc added in v0.6.0

type RenderFunc func(text string, width int) string

RenderFunc renders complete markdown at a wrap width. It is only ever called with whole blocks, never with a partial one.

Its result carries the renderer's own SGR sequences. Do not wrap that result in a lipgloss.Style carrying a foreground: lipgloss re-asserts its color after every embedded reset, repainting each token the renderer colored. A base color belongs inside the render function, not around its output.

type RevisedItem added in v0.8.1

type RevisedItem interface {
	Revision() int
}

RevisedItem is the optional half of ListItem: an item whose content changes under a fixed ID reports a revision that changes with it, and the memo drops the stale entry on its own. It is the declarative alternative to calling MemoList.Invalidate — useful when the item is a view onto data the caller mutates elsewhere. Items that do not implement it are treated as revision 0, which is to say immutable until invalidated.

type SearchView added in v0.2.0

type SearchView struct {
	// contains filtered or unexported fields
}

SearchView is a scrollable text pane with an incremental substring filter and follow-to-bottom behavior — the log/reader viewport most terminal apps rebuild by hand. Feed it lines with SetLines; it renders the subset matching the current query, stays pinned to the bottom as new lines arrive (until the user scrolls up), and toggles a search input on "/".

SearchView owns no rendering chrome of its own beyond the viewport: a host composes the search prompt (see InputView) and any help footer around it, so it drops into an existing panel or tab layout.

func NewSearchView added in v0.2.0

func NewSearchView() SearchView

NewSearchView returns a SearchView following the bottom of an empty pane.

func (*SearchView) Filtered added in v0.2.0

func (s *SearchView) Filtered() []string

Filtered returns the lines matching the current query (case-insensitive substring); the full slice when the query is empty. Matching is done against each line's visible text — ANSI styling is stripped first — so a query never matches the escape codes in a colored line, while the returned lines keep their styling for display.

func (*SearchView) InputView added in v0.2.0

func (s *SearchView) InputView() string

InputView renders the search input, for a host that wants to show the live "Search: …" prompt in its footer.

func (*SearchView) Query added in v0.2.0

func (s *SearchView) Query() string

Query is the current filter text.

func (*SearchView) Searching added in v0.2.0

func (s *SearchView) Searching() bool

Searching reports whether the search input has focus, so a host can stop treating typed keys as its own navigation while the user is typing a query.

func (*SearchView) SetLines added in v0.2.0

func (s *SearchView) SetLines(lines []string)

SetLines replaces the pane's backing content. The rendered view still applies the current query; call it every frame with fresh data (e.g. tailed logs).

func (*SearchView) Update added in v0.2.0

func (s *SearchView) Update(msg tea.Msg)

Update handles a key message. While the search input is focused, keys type into it (Enter or Esc blur it). Otherwise "/" opens search, Esc clears the query and re-follows, and Up/Down scroll — scrolling away from the bottom stops follow, scrolling back to it resumes. Non-key messages are ignored.

func (*SearchView) View added in v0.2.0

func (s *SearchView) View(width, height int) string

View lays the filtered content into a width×height viewport, keeping the pane pinned to the bottom while following.

type Segment added in v0.8.0

type Segment struct {
	Label string
	Value string
	Share float64
	Style lipgloss.Style
}

Segment is one attributed slice of a SegmentBar: a legend label, a preformatted value cell, its share of the whole in [0, 1], and the style painting both its bar cells and its legend swatch.

type SelectItem added in v0.4.0

type SelectItem struct {
	// Label is the primary text. It is filtered against and middle-truncated
	// when the pane is too narrow, so the distinctive tail of a long name
	// (a quantization suffix, a path leaf) survives.
	Label string
	// Trailing is right-aligned metadata — "local · 14.2 GB", "2h ago".
	Trailing string
	// Marked draws Trailing in the theme's Yellow instead of muted, for the
	// one row that is already current ("active").
	Marked bool
	// Key is the host's identifier for this row. Falls back to Label.
	Key string
}

SelectItem is one row of a SelectList: a label the user picks by, plus optional right-aligned metadata. Key is what the host resolves the selection back to; when empty, Label stands in.

type SelectList added in v0.4.0

type SelectList struct {
	// contains filtered or unexported fields
}

SelectList is a filterable, cursor-driven picker — the "choose one of these, and let me type to narrow it down" list most terminal apps rebuild by hand around bubbles/list and a custom delegate.

It complements SearchView, which filters a scrolling text pane but has no selection. Like SearchView it owns no chrome beyond its rows: a host composes the surrounding panel, footer, and counter (see SelectList.Counter).

func NewSelectList added in v0.4.0

func NewSelectList(theme Theme) SelectList

NewSelectList returns an empty SelectList drawing from theme, with the filter closed.

func (*SelectList) Counter added in v0.4.0

func (s *SelectList) Counter() string

Counter is the "9 of 21" progress label, or "21" when nothing is filtered out — there is no news in "21 of 21".

func (*SelectList) Filtered added in v0.4.0

func (s *SelectList) Filtered() []SelectItem

Filtered returns the rows matching the current query (case-insensitive substring over Label), or every row when the query is empty.

func (*SelectList) Filtering added in v0.4.0

func (s *SelectList) Filtering() bool

Filtering reports whether the filter input has focus, so a host can stop treating typed keys as its own shortcuts while the user narrows the list.

func (*SelectList) Focus added in v0.4.0

func (s *SelectList) Focus()

Focus opens the filter and puts the cursor in it, for a host that wants the list to be type-to-filter from the moment it opens.

func (*SelectList) InputView added in v0.4.0

func (s *SelectList) InputView() string

InputView renders the filter input, for a host showing a live prompt.

func (*SelectList) Query added in v0.4.0

func (s *SelectList) Query() string

Query is the current filter text.

func (*SelectList) Selected added in v0.4.0

func (s *SelectList) Selected() (SelectItem, bool)

Selected is the row under the cursor. The bool is false when the filter matches nothing.

func (*SelectList) SelectedKey added in v0.4.0

func (s *SelectList) SelectedKey() string

SelectedKey is SelectList.Selected's Key, or "" when nothing matches.

func (*SelectList) SetItems added in v0.4.0

func (s *SelectList) SetItems(items []SelectItem)

SetItems replaces the backing rows, clamping the cursor into the new filtered set. Call it whenever the source data changes.

func (*SelectList) SetTheme added in v0.4.0

func (s *SelectList) SetTheme(theme Theme)

SetTheme reskins the list, for a host that rebuilds its palette when the terminal background changes.

func (*SelectList) Update added in v0.4.0

func (s *SelectList) Update(msg tea.Msg)

Update handles a key message. While the filter is focused, printable keys type into it and Enter or Esc closes it (Esc also clearing the query). Otherwise "/" opens the filter and Up/Down move the cursor. Enter is left for the host to act on — SelectList never decides what picking means. Non-key messages are ignored.

func (*SelectList) View added in v0.4.0

func (s *SelectList) View(width, height int) string

View renders up to height rows in width columns, scrolling a window that keeps the cursor visible. The selected row is marked with "›" and drawn in the theme's Brand color; labels too long for the pane are middle-truncated.

type Selection added in v0.7.0

type Selection struct {
	// contains filtered or unexported fields
}

Selection is a drag-selected range over a rendered frame. It holds two cells and nothing else: the frame is passed to Selection.Paint and Selection.Text each time, so a selection never goes stale against content that has been re-rendered underneath it.

Selection is linear, the way a text editor and a terminal are: the first row runs from the anchor to the end of the line, whole rows follow, and the last row ends at the cursor. That is right for a transcript and wrong for a frame laid out in columns, where a drag inside one panel still picks up the panels beside it — see issue #18 for block and region-aware modes.

The zero value is an empty selection, ready to use.

func (*Selection) Begin added in v0.7.0

func (s *Selection) Begin(c Cell)

Begin anchors a new selection at c, as a mouse press does.

func (*Selection) Clear added in v0.7.0

func (s *Selection) Clear()

Clear drops the selection.

func (Selection) Empty added in v0.7.0

func (s Selection) Empty() bool

Empty reports whether there is nothing to paint or copy — no selection, or one that never moved off its anchor (a plain click).

func (*Selection) Extend added in v0.7.0

func (s *Selection) Extend(c Cell)

Extend moves the loose end to c, as a drag does. It is a no-op until Begin.

func (Selection) Paint added in v0.7.0

func (s Selection) Paint(view string, style lipgloss.Style) string

Paint returns view with the selected range drawn in style. Rows and columns outside the frame are ignored, so a drag past the last line is harmless.

func (Selection) Text added in v0.7.0

func (s Selection) Text(view string) string

Text is the plain text of the selected range, newline-separated and stripped of escapes — what a copy gesture puts on the clipboard. Trailing blanks are trimmed per line, because a selection that runs past the end of a line is selecting the padding, not the text.

type SetThemeMsg

type SetThemeMsg struct{ Theme Theme }

SetThemeMsg re-themes the Frame's chrome at runtime. Send it with SetTheme.

type Status added in v0.2.2

type Status struct {
	// contains filtered or unexported fields
}

Status tracks a "press again to confirm" destructive-action flow together with the success/error message it leaves behind. One Status handles one pending action at a time; the target string disambiguates which item is armed.

func (*Status) AppendRows added in v0.2.2

func (s *Status) AppendRows(t Theme, rows []string) []string

AppendRows appends a rendered status line to rows: a yellow error row if an error is set, otherwise a green success row if a message is set, otherwise rows unchanged.

func (*Status) Clear added in v0.2.2

func (s *Status) Clear()

Clear resets everything: armed target and any result message.

func (*Status) Confirm added in v0.2.2

func (s *Status) Confirm(target string, confirm bool, fire func() tea.Cmd) tea.Cmd

Confirm implements press-again-to-confirm. On the first press for a target it arms that target (clearing any prior messages) and returns nil; on a second press of the same target it disarms and returns fire(). The confirm flag marks a dedicated confirm key (e.g. "y"): when true it will only fire an already armed target, never arm a new one.

func (*Status) Disarm added in v0.2.2

func (s *Status) Disarm()

Disarm clears any armed target without touching the result message.

func (*Status) Pending added in v0.2.2

func (s *Status) Pending() string

Pending returns the currently armed target, or "" when nothing is armed.

func (*Status) SetError added in v0.2.2

func (s *Status) SetError(msg string)

SetError shows msg as an error, superseding any success message. It leaves the armed target untouched.

func (*Status) SetResult added in v0.2.2

func (s *Status) SetResult(err error, okMsg string)

SetResult records the outcome of a fired action: on success it shows okMsg, on error it shows err.Error(). Either way it clears the pending target.

type StatusFunc

type StatusFunc func() (text string, level Level)

StatusFunc supplies the footer status line. Return an empty string to show the default "Ready".

type StreamingMarkdown added in v0.6.0

type StreamingMarkdown struct {
	// contains filtered or unexported fields
}

StreamingMarkdown renders markdown that is still arriving — an LLM answer, a tailed document — without waiting for the stream to end. Markdown parsers need whole blocks: an unclosed fence swallows everything after it, so text rendered mid-block comes out wrong. The usual workaround is to show raw text until the stream closes and format it all at once, which is why so many terminal chat UIs "pop" into formatting at the end of a turn.

StreamingMarkdown splits the buffer instead. Everything up to the last provably-safe block boundary is rendered and cached; the unsettled remainder is wrapped as raw text and redrawn each frame. Because a TUI repaints continuously, that tail only has to be readable, never correct — it is replaced by real output the moment its block closes. A long code fence streams as plain text and snaps to highlighted when it closes, rather than sitting blank until then.

StreamingMarkdown owns no colors and no markdown engine: it takes a RenderFunc and holds only a cache, so the root package stays free of a markdown dependency. See github.com/antonikliment/tuikit/markdown for a glamour-backed implementation, or supply your own.

func NewStreamingMarkdown added in v0.6.0

func NewStreamingMarkdown(render RenderFunc, opts ...StreamingOption) StreamingMarkdown

NewStreamingMarkdown returns a StreamingMarkdown that formats settled blocks with render.

func (*StreamingMarkdown) Render added in v0.6.0

func (s *StreamingMarkdown) Render(text string, width int) string

Render lays out the buffer at the given width: settled blocks formatted, and the unsettled tail rendered with its open constructs closed (or wrapped verbatim under WithRawTail). Pass the whole buffer every frame — StreamingMarkdown keeps no copy of the text, so a caller is free to edit, truncate or replace it between calls, and size is an argument rather than state, matching the rest of the kit.

The result carries no styling of its own, but the render function's does — see RenderFunc before wrapping the result in a style.

Block-level reflow survives either way: the tail is still a partial block, so a list gains its bullet and a table its borders when the block settles. What speculative closing removes is the inline half of that jump, and the markers.

func (*StreamingMarkdown) Settled added in v0.6.0

func (s *StreamingMarkdown) Settled() int

Settled reports how many bytes of the last rendered buffer had provably settled, and so were rendered from their own source rather than speculatively closed. It exists for diagnostics — a host that wants to mark the boundary, or report how far behind the formatting is running.

type StreamingOption added in v0.6.0

type StreamingOption func(*StreamingMarkdown)

StreamingOption configures a StreamingMarkdown.

func WithRawTail added in v0.6.0

func WithRawTail() StreamingOption

WithRawTail wraps the unsettled tail verbatim instead of rendering it, which is what the component did before speculative closing existed.

The default costs a render of the tail on every frame it changes, where this costs a wrap: 129µs against 3.2µs in BenchmarkRenderTail. That is immaterial against a 16ms frame in one pane, and worth declining in a program repainting several of them at speed. The visible difference is markers — "**bold" and backticks appear on screen — and a partial fence arriving as plain text rather than highlighted.

type Theme

type Theme struct {
	Green  color.Color
	Blue   color.Color
	Yellow color.Color
	Red    color.Color
	Cyan   color.Color
	Muted  color.Color
	Brand  color.Color

	// TabActiveFg is the foreground drawn on a filled active tab chip.
	TabActiveFg color.Color
	// FocusBorder is the border color of a focused Panel.
	FocusBorder color.Color
}

Theme is the configurable palette every tuikit component draws from. Swap the colors (or build one from scratch) to reskin the whole kit; nothing else in the library hardcodes a color.

func DefaultTheme

func DefaultTheme() Theme

DefaultTheme returns the stock 16-color-safe palette.

func (Theme) Accent

func (t Theme) Accent(c color.Color) lipgloss.Style

Accent renders text in the given accent color.

func (Theme) ActionRow added in v0.2.0

func (t Theme) ActionRow(accent color.Color, selected int, labels []string, focused bool) string

ActionRow renders a labelled row of selectable actions, e.g.

Actions:  Start  [Stop]  Restart

The "Actions:" label is drawn in accent. When focused, the label at selected is bracketed and highlighted; the rest are muted. When not focused the whole row is muted, so a page can show which actions exist without implying the row is live.

func (Theme) BrandStyle

func (t Theme) BrandStyle() lipgloss.Style

BrandStyle renders the app/brand name.

func (Theme) EmptyPanel added in v0.2.2

func (t Theme) EmptyPanel(accent color.Color, width, height int, msg string) string

EmptyPanel renders muted placeholder text inside an unfocused panel of the given accent and size — the "nothing selected" state for a detail pane.

func (Theme) LevelStyle added in v0.3.1

func (t Theme) LevelStyle(level Level) lipgloss.Style

LevelStyle is the color a Level is drawn in: green, yellow, red, and muted for LevelInfo. Pairing it with ClassifyStatus turns a status word into a styled one without the caller owning a palette.

func (Theme) MutedStyle

func (t Theme) MutedStyle() lipgloss.Style

MutedStyle renders de-emphasized text.

func (Theme) Pairs added in v0.3.1

func (t Theme) Pairs(paint Painter, keys, values []string, gap int) string

Pairs renders keys and values as a two-column block with the keys muted and padded to a common width. Order is the caller's: sort the keys first if the output has to be stable.

Keys without a matching value render with an empty value rather than panicking, so a mismatched pair of slices degrades instead of crashing.

The result is newline-terminated, ready for IndentLines if it belongs inside a block.

func (Theme) PanelStyle

func (t Theme) PanelStyle(accent color.Color, focused bool) lipgloss.Style

PanelStyle returns a bordered-panel style in the given accent. When focused it swaps to a double border in the theme's FocusBorder color.

func (Theme) Rule

func (t Theme) Rule(width int) string

Rule renders a horizontal muted divider.

func (Theme) StatusTitle

func (t Theme) StatusTitle(title, status string, titleColor, statusColor color.Color, width int) string

StatusTitle renders a "Title ............ ● status" header line, the title in titleColor and the status dot in statusColor, filling width.

func (Theme) StatusWord added in v0.3.1

func (t Theme) StatusWord(paint Painter, word string) string

StatusWord paints a status word by what ClassifyStatus makes of it: green for a healthy state, red for a failure, yellow for anything unfamiliar.

func (Theme) SubtleStyle

func (t Theme) SubtleStyle() lipgloss.Style

SubtleStyle is MutedStyle, dimmer still — for footer/status chrome.

func (Theme) TabStrip

func (t Theme) TabStrip(titles []string, accents []color.Color, active int) string

TabStrip renders a row of labelled tab chips. The active tab is drawn as a three-sided box — border on left, top and right, open at the bottom — in its accent color, so it reads as a folder tab sitting on the panel below it. The rest are muted labels, bottom-aligned to the active tab's label row. Titles are pre-formatted by the caller (e.g. with counts). len(titles) must equal len(accents).

func (Theme) TabbedPanel

func (t Theme) TabbedPanel(titles []string, accents []color.Color, active, width, height int, body string) string

TabbedPanel renders a row of tabs joined seamlessly to a content panel: the active tab opens directly into the panel (its bottom edge is notched out of the panel's top border, so there is no dividing line), and both share the active tab's accent color. Inactive tabs are muted labels. width and height are the total footprint; body is the panel content.

func (Theme) Table added in v0.3.1

func (t Theme) Table(paint Painter, header []string, rows [][]string, gap int) string

Table renders header and rows as aligned columns, one line per row, with the header upper-cased and muted.

It is the rung above Columns and JoinCells: those measure and lay out, this is the loop every caller writes on top of them. Widths are measured over the header as well as the body, so a column never renders narrower than its own title. Short rows are padded out by JoinCells rather than dropped.

The result is newline-terminated, ready for IndentLines if it belongs inside a block.

Directories

Path Synopsis
examples
demo command
Command demo is a runnable showcase of the tuikit frame kit: a Frame with several pages exercising the numbered navigation, TabStrip sub-tabs, Panel, a scrolling viewport, the layout helpers, and the InputCapturer guard.
Command demo is a runnable showcase of the tuikit frame kit: a Frame with several pages exercising the numbered navigation, TabStrip sub-tabs, Panel, a scrolling viewport, the layout helpers, and the InputCapturer guard.
diffview command
Command diffview shows tuikit.DiffView: the same edit rendered unified and side-by-side, syntax-highlighted, with the changed words inside a modified line pair picked out.
Command diffview shows tuikit.DiffView: the same edit rendered unified and side-by-side, syntax-highlighted, with the changed words inside a modified line pair picked out.
memolist command
Command memolist is a runnable harness for tuikit.MemoList: a 5,000-message transcript in a 20-line viewport, with a live count of how many messages were actually rendered on the current frame.
Command memolist is a runnable harness for tuikit.MemoList: a 5,000-message transcript in a 20-line viewport, with a live count of how many messages were actually rendered on the current frame.
selection command
Command selection shows tuikit.Selection: a mouse drag over a rendered frame, painted in place and copied out as plain text.
Command selection shows tuikit.Selection: a mouse drag over a rendered frame, painted in place and copied out as plain text.
streaming command
Command streaming is a runnable harness for tuikit.StreamingMarkdown: an endless generated markdown document, streamed a character at a time, so the boundary handling can be watched rather than inferred from tests.
Command streaming is a runnable harness for tuikit.StreamingMarkdown: an endless generated markdown document, streamed a character at a time, so the boundary handling can be watched rather than inferred from tests.
syntax command
Command syntax shows fenced code under different chroma stylesheets, switched while the program runs.
Command syntax shows fenced code under different chroma stylesheets, switched while the program runs.
themed command
Command themed showcases live theme switching: press "t" to cycle palettes.
Command themed showcases live theme switching: press "t" to cycle palettes.
Package markdown supplies a glamour-backed tuikit.RenderFunc for tuikit.StreamingMarkdown, styled from a tuikit.Theme.
Package markdown supplies a glamour-backed tuikit.RenderFunc for tuikit.StreamingMarkdown, styled from a tuikit.Theme.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL