tui

package
v0.2.15 Latest Latest
Warning

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

Go to latest
Published: Apr 24, 2026 License: MIT Imports: 18 Imported by: 0

Documentation

Overview

Package tui provides shared utilities for terminal UI components.

notificationrow.go contains ActionRow and ActionBadge, the bottom-row UI for action buttons (refresh, send, save) and the notification reopen badge.

Index

Constants

View Source
const (
	// Navigation
	KeyUp    = "up"
	KeyDown  = "down"
	KeyCtrlP = "ctrl+p" // up
	KeyCtrlN = "ctrl+n" // down
	KeyLeft  = "left"
	KeyRight = "right"

	// Vim-style navigation (use only when text input is inactive)
	KeyJ             = "j"
	KeyK             = "k"
	KeyH             = "h"
	KeyL             = "l"
	KeyG             = "g"      // first press of gg (go to top)
	KeyShiftG        = "G"      // go to bottom
	KeyYank          = "ctrl+y" // single 'y' key suffers when the cursor is in TextInput
	KeyRefresh       = "ctrl+r"
	KeySend          = "ctrl+o"
	KeySave          = "ctrl+s"
	KeySaveQuery     = "ctrl+q"
	KeyCreateRequest = "ctrl+x"
	KeySlash         = "/" // vim-style search trigger
	KeyAt            = "@" // reopen or hide the most recent notification

	// Actions
	KeyEnter    = "enter"
	KeyTab      = "tab"
	KeySpace    = " "
	KeyShiftTab = "shift+tab" // Reverse tab - navigate backward
	KeyQuit     = "ctrl+c"    // Force quit - always works
	KeyCancel   = "esc"       // Cancel/back - context aware

)

Common Key binding and other constants single source of truth for all TUI components.

View Source
const (
	// Column widths
	LeftPanelPct       = 30
	MinLeftPanelWidth  = 26
	MinRightPanelWidth = 32

	// Right panel vertical split (percentage of contentHeight)
	DetailTopPct = 40

	// Fixed-height rows (lines). Subtract these from the total
	// vertical budget before giving the remainder to scrollable
	// content areas.
	SearchBoxHeight = 3 // top border + input + bottom border
	StatusRowHeight = 1 // "N/M operations" line
	HelpBarHeight   = 1 // full-width help bar below both columns
)
View Source
const DefaultScrollMargin = 3

DefaultScrollMargin is the number of lines kept visible above/below the cursor when scrolling inside a viewport-backed list.

Variables

View Source
var (
	ColorPrimary   = lipgloss.Color("4") // blue
	ColorSecondary = lipgloss.Color("5") // magenta
	ColorMuted     = lipgloss.Color("8") // gray (bright black)
	ColorWarn      = lipgloss.Color("3") // yellow
	ColorError     = lipgloss.Color("1") // red
	ColorSuccess   = lipgloss.Color("2") // green
)

Semantic colors using basic ANSI (0-15). These are defined by the terminal's own color scheme, so they automatically adapt when the user switches between dark and light themes.

View Source
var (
	// TitleStyle for section titles
	TitleStyle = lipgloss.NewStyle().
				Bold(true).
				Foreground(ColorPrimary)

	// MutedTitleStyle for unfocused section titles
	MutedTitleStyle = lipgloss.NewStyle().
					Bold(true).
					Foreground(ColorMuted)

	// SubtitleStyle for secondary titles
	SubtitleStyle = lipgloss.NewStyle().
					Foreground(ColorSecondary)

	// HelpStyle for inline help text (muted gray)
	HelpStyle = lipgloss.NewStyle().
				Foreground(ColorMuted)

	// HelpBarStyle for the bottom help bar — more noticeable than
	// HelpStyle but not as prominent as body text.
	HelpBarStyle = lipgloss.NewStyle().
					Foreground(ColorMuted).
					Italic(true)

	// BorderStyle for boxes with borders
	BorderStyle = lipgloss.NewStyle().
				Border(lipgloss.RoundedBorder()).
				BorderForeground(ColorMuted)

	// FocusedInputStyle for the actively focused input field
	FocusedInputStyle = BorderStyle.Padding(0, 1).
						BorderForeground(ColorPrimary)

	// InputStyle for unfocused/locked input fields
	InputStyle = BorderStyle.Padding(0, 1)

	// ActionChipStyle renders compact action controls.
	ActionChipStyle = lipgloss.NewStyle().
					Bold(true).
					Padding(0, 1)

	// NotificationBadgeBaseStyle renders the @ reopen badge.
	NotificationBadgeBaseStyle = lipgloss.NewStyle().
								Bold(true).
								Padding(0, 1)
)
View Source
var BoxStyle = lipgloss.NewStyle().
	Border(lipgloss.RoundedBorder()).
	BorderForeground(ColorMuted).
	Padding(1, 1)

BoxStyle for full-screen content containers with rounded border and padding.

View Source
var SpinnerFrames = []rune{'|', '/', '-', '\\'}

Functions

func ClampCursor

func ClampCursor(cursor, maxIndex int) int

ClampCursor ensures cursor is within valid bounds [0, maxIndex]. Useful after filtering reduces the list size.

func CopyToClipboard

func CopyToClipboard(text string) tea.Cmd

CopyToClipboard returns a tea.Cmd that writes text to the system clipboard. The result is delivered as a CopiedMsg so the caller can show feedback or handle errors.

func DistributeSpace

func DistributeSpace(total int, entries []LayoutEntry) []int

DistributeSpace splits total pixels among entries proportionally by weight, guaranteeing each entry at least MinSize. Rounding remainders go to the first entries. If total < sum of MinSizes, every entry still gets its MinSize (caller handles the overflow).

func FileItems

func FileItems() ([]string, error)

func Hit

func Hit(id string, msg tea.MouseMsg) bool

Hit reports whether the given mouse event landed inside the provided zone.

func IsLeftClick

func IsLeftClick(msg tea.MouseMsg) bool

IsLeftClick reports whether msg is a left-button release event.

func MoveCursorDown

func MoveCursorDown(cursor, maxIndex int) int

MoveCursorDown increments cursor position, respecting upper bound. maxIndex is the maximum valid index (typically len(items)-1).

func MoveCursorUp

func MoveCursorUp(cursor int) int

MoveCursorUp decrements cursor position, respecting lower bound of 0.

func NoFilesError

func NoFilesError() error

func OverlayCenter

func OverlayCenter(overlay string, width, height int) string

OverlayCenter places overlay in the center of the canvas.

func RenderBadge

func RenderBadge(text string, color lipgloss.TerminalColor) string

RenderBadge creates a colored badge with the given foreground color.

func RenderChip

func RenderChip(text string, variant ChipVariant, color lipgloss.TerminalColor) string

RenderChip renders compact UI labels used for passive badges, clickable buttons, and solid markers like the @ notification badge.

func RenderChipBlock

func RenderChipBlock(text string, variant ChipVariant, color lipgloss.TerminalColor, width, height int) string

func RunSelector

func RunSelector(
	items []string,
	prompt string,
	emptyErr error,
	helpMessage ...string,
) (string, error)

RunSelector runs a generic selector TUI with the given items and prompt. Returns the selected item, or empty string if cancelled. Returns emptyErr if no items are available.

func RunWithSpinnerAfter

func RunWithSpinnerAfter(message string, task func() (any, error)) (any, error)

func ScanMouseZones

func ScanMouseZones(view string) string

Scan registers all marked regions at the root view without affecting layout.

func SyncViewport

func SyncViewport(vp *viewport.Model, content string, cursorLine, scrollMargin int)

SyncViewport updates vp's content and adjusts its YOffset so that cursorLine stays visible with scrollMargin lines of padding.

func ZoneBounds

func ZoneBounds(id string) (startX, startY, endX, endY int, ok bool)

ZoneBounds returns the stored bounds for the given zone ID.

func ZonePos

func ZonePos(id string, msg tea.MouseMsg) (int, int)

ZonePos reports the mouse position relative to the given zone.

Types

type ActionBadge

type ActionBadge struct {
	Label    string
	Key      string
	Severity NotificationSeverity
	Visible  bool
}

type ActionItem

type ActionItem struct {
	ID      string
	Label   string
	Key     string
	Enabled bool
}

type ActionRow

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

ActionRow renders a compact bottom-left row for future gql actions such as refresh, send, and save. It can also show an optional leading badge like the notification reopen marker.

func NewActionRow

func NewActionRow(items ...ActionItem) ActionRow

func (*ActionRow) BadgeListView

func (r *ActionRow) BadgeListView() string

func (*ActionRow) BadgeView

func (r *ActionRow) BadgeView() string

func (*ActionRow) HandleKey

func (r *ActionRow) HandleKey(key string) (id string, handled bool)

func (*ActionRow) HandleMouse

func (r *ActionRow) HandleMouse(msg tea.MouseMsg) (id string, handled bool)

func (*ActionRow) SetBadge

func (r *ActionRow) SetBadge(badge ActionBadge)

func (*ActionRow) SetItems

func (r *ActionRow) SetItems(items []ActionItem)

func (*ActionRow) View

func (r *ActionRow) View() string

func (*ActionRow) ViewColumn

func (r *ActionRow) ViewColumn(width, height int) string

func (*ActionRow) ViewList

func (r *ActionRow) ViewList() string

type ChipVariant

type ChipVariant string
const (
	ChipVariantBadge  ChipVariant = "badge"
	ChipVariantButton ChipVariant = "button"
	ChipVariantSolid  ChipVariant = "solid"
)

type CopiedMsg

type CopiedMsg struct{ Err error }

CopiedMsg is sent after a clipboard write attempt. Err is nil on success.

type Dropdown struct {
	Label    string
	Options  []string
	Selected int
	// contains filtered or unexported fields
}

Not a full tea.Model — embed in a parent model and call Update/View explicitly, similar to Panel and Toggle.

func NewDropdown

func NewDropdown(label string, options []string, initial int) Dropdown

NewDropdown creates a Dropdown with the given label, options, and initially selected index. Out-of-range initial values clamp to 0.

func (d *Dropdown) Blur()

Blur removes focus and collapses the dropdown if expanded.

func (d Dropdown) Cursor() int

Cursor returns the current cursor index inside the expanded list.

func (d *Dropdown) Expand()

Expand opens the dropdown and aligns the cursor with the selected option.

func (d Dropdown) Expanded() bool

Expanded reports whether the option list is currently visible.

func (d *Dropdown) Focus()

Focus marks the dropdown as focused.

func (d Dropdown) Focused() bool

Focused reports whether the dropdown currently has focus.

func (d Dropdown) Init() tea.Cmd

Init returns nil; Dropdown has no startup commands.

func (d *Dropdown) Select(index int)

Select chooses the option at index and collapses the dropdown.

func (d Dropdown) Update(msg tea.Msg) (Dropdown, tea.Cmd)

Update processes key messages based on the current expand state. Collapsed: Enter/Space expand the list. Expanded: arrow keys move the cursor, Enter/Space select, Esc collapses without changing.

func (d Dropdown) Value() string

Value returns the currently selected option string, or empty if there are no options.

func (d Dropdown) View() string

View renders the dropdown. Collapsed: indicator + selected value. Expanded: full option list with cursor highlight.

type FilterableList

type FilterableList struct {
	Filtered  []string
	Cursor    int
	TextInput TextInput
	// contains filtered or unexported fields
}

FilterableList is a headless data component that manages a string list with text-based filtering, cursor tracking, and item rendering. It does not implement tea.Model; embed it in a Bubble Tea model (like SelectorModel or apicaller.helperPane) to build interactive selectors on top.

func NewFilterableList

func NewFilterableList(
	items []string,
	prompt, placeholder string,
	requireInput bool,
) FilterableList

func (*FilterableList) ClearFilter

func (f *FilterableList) ClearFilter()

func (*FilterableList) HasFilterValue

func (f *FilterableList) HasFilterValue() bool

func (*FilterableList) RenderItems

func (f *FilterableList) RenderItems() (string, int)

RenderItems renders the filtered list with a cursor indicator on the selected item. It returns the rendered content and the line number of the cursor, which callers can pass to SyncViewport for scroll tracking.

func (*FilterableList) RenderItemsWidth

func (f *FilterableList) RenderItemsWidth(maxWidth int) (string, int)

func (*FilterableList) SelectCurrent

func (f *FilterableList) SelectCurrent() (string, bool)

func (*FilterableList) UpdateInput

func (f *FilterableList) UpdateInput(msg tea.Msg) tea.Cmd

type FocusRing

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

FocusRing tracks which panel is focused and whether the user is in typing mode. Index -1 means the left panel (search/list) is focused; 0+ indexes into the panels slice.

func NewFocusRing

func NewFocusRing(panels []*Panel) FocusRing

NewFocusRing creates a ring starting on the left panel (index -1).

func (*FocusRing) FocusByNumber

func (f *FocusRing) FocusByNumber(num int) bool

FocusByNumber jumps to the panel with the given Number field. Number 1 focuses the left panel. Returns false if no match found. Exits typing mode on switch.

func (*FocusRing) HandleKey

func (f *FocusRing) HandleKey(key string) (consumed, quit bool)

TODO-gql: gqlexplorer.handleKey duplicates Esc/Tab/Enter/number-key logic instead of delegating to HandleKey. Consolidate in Phase 3 when multiple editable panels make the duplication painful.

HandleKey processes focus-related keys. Returns two bools:

  • consumed: true if the key was handled (caller should not process it further)
  • quit: true if Esc was pressed while already not typing (caller should exit)

func (*FocusRing) IsFocused

func (f *FocusRing) IsFocused(p *Panel) bool

IsFocused returns true if the given panel is the currently focused one.

func (*FocusRing) LeftFocused

func (f *FocusRing) LeftFocused() bool

LeftFocused returns true when the left panel (search/list) has focus.

func (*FocusRing) Next

func (f *FocusRing) Next()

Next advances focus to the next panel, wrapping from the last panel back to the left panel (-1). Exits typing mode on switch.

func (*FocusRing) Prev

func (f *FocusRing) Prev()

Prev moves focus to the previous panel, wrapping from the left panel back to the last panel. Exits typing mode on switch.

func (*FocusRing) SetTyping

func (f *FocusRing) SetTyping(v bool)

func (*FocusRing) Typing

func (f *FocusRing) Typing() bool

type LayoutEntry

type LayoutEntry struct {
	Weight  int
	MinSize int
}

type MouseZone

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

MouseZone provides stable, collision-free zone IDs for one component or model. Actionable views should mark click targets with IDs derived from the same zone.

func NewMouseZone

func NewMouseZone() MouseZone

NewMouseZone initializes the shared zone manager and returns a unique prefix that can be used to create stable IDs for one component instance.

func (MouseZone) ID

func (z MouseZone) ID(parts ...string) string

ID builds a stable namespaced ID for a clickable/focusable region.

func (MouseZone) Mark

func (z MouseZone) Mark(id, view string) string

Mark wraps an actionable view region with a zone marker.

type Notification

type Notification struct {
	Severity  NotificationSeverity
	Message   string
	CreatedAt time.Time
	ExpiresAt time.Time
}

type NotificationCenter

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

NotificationCenter manages one active notification plus the last dismissed message. Enqueue/Update integrate with Bubble Tea; callers can render either a compact notification or a top-overlay modal.

func NewNotificationCenter

func NewNotificationCenter() NotificationCenter

func (*NotificationCenter) CopyText

func (n *NotificationCenter) CopyText() string

func (*NotificationCenter) Enqueue

func (n *NotificationCenter) Enqueue(severity NotificationSeverity, message string) tea.Cmd

Enqueue shows a notification and schedules expiry based on estimated reading time.

func (*NotificationCenter) HandleKey

func (n *NotificationCenter) HandleKey(key string) (handled bool, copyText string)

HandleKey handles notification-local interactions. For ctrl+y it returns the displayed message so the parent can call CopyToClipboard.

func (*NotificationCenter) HasLast

func (n *NotificationCenter) HasLast() bool

func (*NotificationCenter) Render

func (n *NotificationCenter) Render(width, height int) string

func (*NotificationCenter) RenderModal

func (n *NotificationCenter) RenderModal(width, height int) string

func (*NotificationCenter) Severity

func (*NotificationCenter) ToggleLast

func (n *NotificationCenter) ToggleLast() bool

func (*NotificationCenter) Update

func (n *NotificationCenter) Update(msg tea.Msg) tea.Cmd

func (*NotificationCenter) Visible

func (n *NotificationCenter) Visible() bool

type NotificationSeverity

type NotificationSeverity string
const (
	NotificationInfo  NotificationSeverity = "info"
	NotificationWarn  NotificationSeverity = "warn"
	NotificationError NotificationSeverity = "error"
)

type Panel

type Panel struct {
	Number int
	Header string
	Footer string
	Label  string
	// contains filtered or unexported fields
}

Panel is a reusable bordered viewport box for right-side content panels. Parent owns layout sizing; Panel owns viewport state, border rendering, and content caching. Not a full tea.Model — embed in a parent model and call Update/View explicitly.

func (*Panel) CanRender

func (p *Panel) CanRender() bool

CanRender returns false when outer dimensions are too small to fit the border frame, avoiding lipgloss rendering artifacts.

func (*Panel) EnsureVisible

func (p *Panel) EnsureVisible(line, colStart, colEnd int)

EnsureVisible scrolls both axes so the given line and column range are on screen.

func (*Panel) GotoBottom

func (p *Panel) GotoBottom()

GotoBottom sets the viewport scroll position to the bottom.

func (*Panel) GotoTop

func (p *Panel) GotoTop()

GotoTop resets the viewport scroll position to the top.

func (*Panel) Resize

func (p *Panel) Resize(outerW, outerH int)

func (*Panel) ScrollPercent

func (p *Panel) ScrollPercent() float64

ScrollPercent returns the viewport's current scroll position as 0.0–1.0.

func (*Panel) SetContent

func (p *Panel) SetContent(content, cacheKey string) bool

SetContent updates viewport content. Returns true if the content was updated, false if the cacheKey matched and the update was skipped. Pass an empty cacheKey to force update every time.

func (*Panel) SetHeader

func (p *Panel) SetHeader(header string)

SetHeader sets the sticky header text and recalculates viewport height.

func (*Panel) SyncContent

func (p *Panel) SyncContent(content string, cursorLine int)

SyncContent sets content and scrolls so cursorLine stays visible.

func (*Panel) Update

func (p *Panel) Update(msg tea.Msg) tea.Cmd

Update forwards messages (scroll, mouse) to the inner viewport. Parent should call this only when this panel is focused.

func (*Panel) View

func (p *Panel) View(focused bool) string

View renders the panel as a bordered box. When Number > 0, a styled [N] label appears inside the box at the bottom-right. Focused panels use ColorPrimary for the border and label, unfocused panels use ColorMuted.

func (*Panel) Width

func (p *Panel) Width() int

Width returns the inner viewport width (content area, excluding borders).

type PanelSearch

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

PanelSearch provides reusable search-input state, match navigation, key handling, and footer rendering. The caller owns match computation; PanelSearch owns the input, index list, cursor, and UI chrome.

func NewPanelSearch

func NewPanelSearch() PanelSearch

NewPanelSearch creates a PanelSearch with up/down keys unbound so the host panel can use them for navigation.

func (*PanelSearch) Active

func (ps *PanelSearch) Active() bool

Active returns true while the search input is open.

func (*PanelSearch) CurrentMatch

func (ps *PanelSearch) CurrentMatch() int

CurrentMatch returns the index stored at the current match cursor, or -1 when there are no matches.

func (*PanelSearch) CycleNext

func (ps *PanelSearch) CycleNext()

CycleNext moves to the next match, wrapping around.

func (*PanelSearch) CyclePrev

func (ps *PanelSearch) CyclePrev()

CyclePrev moves to the previous match, wrapping around.

func (*PanelSearch) Footer

func (ps *PanelSearch) Footer() string

Footer renders "Search(/) [input] N/M" or "" when inactive.

func (*PanelSearch) HandleKey

func (ps *PanelSearch) HandleKey(msg tea.KeyMsg) (stopped bool, confirmed bool, cmd tea.Cmd)

HandleKey processes key messages while search is active. stopped=true means search ended (confirmed=true for Enter, false for Esc). The caller should re-run match computation after non-stop returns.

func (*PanelSearch) MatchCount

func (ps *PanelSearch) MatchCount() int

MatchCount returns how many matches are currently tracked.

func (*PanelSearch) Query

func (ps *PanelSearch) Query() string

Query returns the current raw search text.

func (*PanelSearch) SetMatches

func (ps *PanelSearch) SetMatches(indices []int)

SetMatches replaces the match list and resets the cursor to 0.

func (*PanelSearch) SetQuery

func (ps *PanelSearch) SetQuery(value string)

SetQuery replaces the current search text programmatically.

func (*PanelSearch) Start

func (ps *PanelSearch) Start()

Start opens the search input and resets all match state.

func (*PanelSearch) Stop

func (ps *PanelSearch) Stop()

Stop closes the search input and clears matches.

type SelectorModel

type SelectorModel struct {
	FilterableList
	Selected  string
	Cancelled bool
	// contains filtered or unexported fields
}

SelectorModel is the shared single-list picker engine for simple selection flows.

func NewSelector

func NewSelector(items []string, prompt string, customHelp ...string) SelectorModel

func (*SelectorModel) Init

func (m *SelectorModel) Init() tea.Cmd

func (*SelectorModel) Update

func (m *SelectorModel) Update(msg tea.Msg) (tea.Model, tea.Cmd)

func (*SelectorModel) View

func (m *SelectorModel) View() string

type TextInput

type TextInput struct {
	Model textinput.Model
}

TextInput wraps a textinput.Model with reusable behaviors: cursor blinking, message forwarding, and bordered title rendering. Access the inner Model directly for Value(), Reset(), SetValue(), etc.

func NewFilterInput

func NewFilterInput(opts TextInputOpts) TextInput

NewFilterInput creates a TextInput configured for filtering lists. Suggestion keys (up/down/ctrl+p/ctrl+n) are disabled so they can be used for list navigation instead.

func (*TextInput) Init

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

Init returns the blink command to start cursor animation.

func (*TextInput) Update

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

Update forwards messages to the inner textinput model.

func (*TextInput) ViewTitle

func (f *TextInput) ViewTitle() string

ViewTitle renders the textinput inside a bordered title bar.

type TextInputOpts

type TextInputOpts struct {
	Prompt      string
	Placeholder string
	MinWidth    int // minimum character width for the input field; 0 means use placeholder length
}

TextInputOpts configures a TextInput.

type Toggle

type Toggle struct {
	Label string
	Value bool
	// contains filtered or unexported fields
}

Toggle is a reusable checkbox/toggle component for Bubble Tea TUIs. It renders as [x] label (on) or [ ] label (off) and responds to Space/Enter to flip the value. Only responds to keys when focused.

Not a full tea.Model — embed in a parent model and call Update/View explicitly, similar to Panel and TextInput.

func NewToggle

func NewToggle(label string, initial bool) Toggle

NewToggle creates a Toggle with the given label and initial value.

func (*Toggle) Blur

func (t *Toggle) Blur()

Blur removes focus from the toggle.

func (*Toggle) Focus

func (t *Toggle) Focus()

Focus marks the toggle as focused.

func (Toggle) Focused

func (t Toggle) Focused() bool

Focused reports whether the toggle currently has focus.

func (Toggle) Init

func (t Toggle) Init() tea.Cmd

Init returns nil; Toggle has no startup commands.

func (Toggle) Update

func (t Toggle) Update(msg tea.Msg) (Toggle, tea.Cmd)

Update handles key messages. Space and Enter toggle the value when focused. All other messages are ignored. Returns the updated Toggle and any command (always nil for Toggle).

func (Toggle) View

func (t Toggle) View() string

View renders the toggle as [x] label or [ ] label with focus-aware coloring. The checkbox uses ColorPrimary when focused and on, ColorMuted otherwise.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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