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:
- Theme.TabbedPanel — a row of tabs joined seamlessly to a content panel; the active tab opens into the panel with no dividing line.
- Theme.TabStrip — just the row of active/inactive tab chips.
- Theme.PanelStyle and Panel — bordered panels with a focused state.
- SearchView — a searchable viewport with follow-to-bottom behavior.
- StreamingMarkdown — markdown formatted as it arrives, settled blocks rendered and the unfinished tail left raw. Pair it with a RenderFunc; github.com/antonikliment/tuikit/markdown supplies a glamour-backed one, kept in its own package so the markdown stack stays optional.
- DiffView — a diff between two versions of a file, unified or side-by-side, syntax-highlighted by chroma with the changed words inside a modified line pair picked out.
- MemoList — a virtualized list for long scrollback: only the visible window is rendered, each item's block is memoized by ID and revision, and the tail is followed until the user scrolls up.
- Overlay — a dismissable modal popup composited over a finished view, pinnable to any of nine anchors via Alignment.
- Theme.ActionRow — a labelled row of selectable actions.
- Selection and Cell — a mouse drag over a rendered frame, painted with a highlight and copied out as plain text.
- Help and HelpLine — bright full and one-line keyboard help.
- Meter — a fixed-width resource gauge over bubbles/progress.
- Status — a press-again-to-confirm flow with a result message.
- Theme.Table and Theme.Pairs — an aligned block from a header and rows, or from key/value pairs, over the display-width primitives Columns, JoinCells, Pad and Widest, so styled cells still align.
- Painter, PainterFor, Paint, Plain — paint only when the destination is a terminal and NO_COLOR is unset, decided once.
- ClassifyStatus and Theme.StatusWord — grade a status word into a Level and paint it green, red or amber.
- Theme.StatusTitle, Theme.Rule, Theme.EmptyPanel, Field, VerticalSlice, Flow, AdaptiveWidth, Indent, IndentLines, TruncMiddle, Titleize, FormatBytes, TransferredBytes, CoarseDuration, Age — layout and text helpers.
See the docs/examples.md file for copy-paste snippets, and examples/demo for a runnable showcase.
Index ¶
- Constants
- func AdaptiveWidth(total, gap, minimum, maximum int) int
- func Age(at, now time.Time) string
- func CoarseDuration(d time.Duration) time.Duration
- func ColorEnabled(w io.Writer) bool
- func Columns(rows [][]string) []int
- func Field(label, value string) string
- func Flow(width, gap int, blocks []string) string
- func FormatBytes(size int64) string
- func Help() help.Model
- func HelpLine(bindings ...key.Binding) string
- func Indent(text string, depth int) string
- func IndentLines(text string, depth int) string
- func IsTerminalWriter(w io.Writer) bool
- func JoinCells(row []string, widths []int, gap int) string
- func Pad(text string, width int) string
- func Paint(s lipgloss.Style, text string) string
- func Plain(_ lipgloss.Style, text string) string
- func SegmentBar(width int, segments []Segment, free Segment) string
- func SetTheme(theme Theme) tea.Cmd
- func Titleize(name string) string
- func TransferredBytes(received, total int64) string
- func TruncMiddle(s string, width int) string
- func VerticalSlice(content string, offset, height int) string
- func Widest(values []string) int
- type Alignment
- type Cell
- type DiffView
- func (d *DiffView) After(path, content string) *DiffView
- func (d *DiffView) Before(path, content string) *DiffView
- func (d *DiffView) ContextLines(n int) *DiffView
- func (d *DiffView) Highlighter(h Highlighter) *DiffView
- func (d *DiffView) MaxLines(n int) *DiffView
- func (d *DiffView) Painter(p Painter) *DiffView
- func (d *DiffView) Render(width int) string
- func (d *DiffView) RenderSplit(width int) string
- type Frame
- type GlobalKeyFunc
- type Highlighter
- type InputCapturer
- type KeyMap
- type Level
- type ListItem
- type MemoList
- func (l *MemoList) Append(items ...ListItem)
- func (l *MemoList) Following() bool
- func (l *MemoList) Invalidate(id string)
- func (l *MemoList) InvalidateAll()
- func (l *MemoList) Len() int
- func (l *MemoList) Render(width, height int) string
- func (l *MemoList) ScrollBy(delta int)
- func (l *MemoList) ScrollToBottom()
- func (l *MemoList) ScrollToTop()
- func (l *MemoList) SetItems(items []ListItem)
- type Meter
- type Option
- type Overlay
- type Page
- type Painter
- type Panel
- type RenderFunc
- type RevisedItem
- type SearchView
- type Segment
- type SelectItem
- type SelectList
- func (s *SelectList) Counter() string
- func (s *SelectList) Filtered() []SelectItem
- func (s *SelectList) Filtering() bool
- func (s *SelectList) Focus()
- func (s *SelectList) InputView() string
- func (s *SelectList) Query() string
- func (s *SelectList) Selected() (SelectItem, bool)
- func (s *SelectList) SelectedKey() string
- func (s *SelectList) SetItems(items []SelectItem)
- func (s *SelectList) SetTheme(theme Theme)
- func (s *SelectList) Update(msg tea.Msg)
- func (s *SelectList) View(width, height int) string
- type Selection
- type SetThemeMsg
- type Status
- func (s *Status) AppendRows(t Theme, rows []string) []string
- func (s *Status) Clear()
- func (s *Status) Confirm(target string, confirm bool, fire func() tea.Cmd) tea.Cmd
- func (s *Status) Disarm()
- func (s *Status) Pending() string
- func (s *Status) SetError(msg string)
- func (s *Status) SetResult(err error, okMsg string)
- type StatusFunc
- type StreamingMarkdown
- type StreamingOption
- type Theme
- func (t Theme) Accent(c color.Color) lipgloss.Style
- func (t Theme) ActionRow(accent color.Color, selected int, labels []string, focused bool) string
- func (t Theme) BrandStyle() lipgloss.Style
- func (t Theme) EmptyPanel(accent color.Color, width, height int, msg string) string
- func (t Theme) LevelStyle(level Level) lipgloss.Style
- func (t Theme) MutedStyle() lipgloss.Style
- func (t Theme) Pairs(paint Painter, keys, values []string, gap int) string
- func (t Theme) PanelStyle(accent color.Color, focused bool) lipgloss.Style
- func (t Theme) Rule(width int) string
- func (t Theme) StatusTitle(title, status string, titleColor, statusColor color.Color, width int) string
- func (t Theme) StatusWord(paint Painter, word string) string
- func (t Theme) SubtleStyle() lipgloss.Style
- func (t Theme) TabStrip(titles []string, accents []color.Color, active int) string
- func (t Theme) TabbedPanel(titles []string, accents []color.Color, active, width, height int, body string) string
- func (t Theme) Table(paint Painter, header []string, rows [][]string, gap int) string
Constants ¶
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.
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.
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.
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.
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
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
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
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
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
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 Flow ¶
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
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
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
HelpLine renders bindings as a single "key desc • key desc" short-help line using the brightened styles.
func Indent ¶ added in v0.3.0
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
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
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
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
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
Paint is the Painter that always paints, for output already known to be going to a terminal.
func Plain ¶ added in v0.3.1
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
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 ¶
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
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
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
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 ¶
VerticalSlice hard-clips content to height lines starting at offset, so a block never grows past a fixed footprint.
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
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) ContextLines ¶ added in v0.8.1
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
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
Painter sets how styles are applied — pass Plain to render without escapes.
func (*DiffView) RenderSplit ¶ added in v0.8.1
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 ¶
New builds a Frame. Defaults: DefaultTheme, DefaultKeyMap, 120x32 until the first WindowSizeMsg.
func (*Frame) ActivePage ¶
ActivePage returns the current page index.
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
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.
Footer status levels, used to color the status line.
func ClassifyStatus ¶ added in v0.3.1
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
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
Following reports whether the list is stuck to the tail.
func (*MemoList) Invalidate ¶ added in v0.8.1
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) Render ¶ added in v0.8.1
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
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.
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.
type Option ¶
type Option func(*Frame)
Option configures a Frame.
func WithGlobalKeys ¶
func WithGlobalKeys(fn GlobalKeyFunc) Option
WithGlobalKeys registers an app-wide key handler (theme toggle, help, etc.).
func WithStatus ¶
func WithStatus(status StatusFunc) Option
WithStatus sets the footer status provider.
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
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
Content is the text the Overlay was opened with, unwrapped.
func (*Overlay) Open ¶ added in v0.5.0
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
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.
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
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.
type Panel ¶
Panel is a convenience wrapper around PanelStyle with size baked in. Zero Width/Height means "fit content".
type RenderFunc ¶ added in v0.6.0
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
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
Begin anchors a new selection at c, as a mouse press does.
func (Selection) Empty ¶ added in v0.7.0
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
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
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
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
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
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
Pending returns the currently armed target, or "" when nothing is armed.
type StatusFunc ¶
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 (Theme) ActionRow ¶ added in v0.2.0
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 ¶
BrandStyle renders the app/brand name.
func (Theme) EmptyPanel ¶ added in v0.2.2
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
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 ¶
MutedStyle renders de-emphasized text.
func (Theme) Pairs ¶ added in v0.3.1
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 ¶
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) 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
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 ¶
SubtleStyle is MutedStyle, dimmer still — for footer/status chrome.
func (Theme) TabStrip ¶
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
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.
Source Files
¶
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. |


















