dialog

package
v0.2.4 Latest Latest
Warning

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

Go to latest
Published: Aug 17, 2026 License: MIT Imports: 15 Imported by: 0

Documentation

Overview

Package dialog is a domain-free stack of modal overlays for a Bubble Tea application. A Dialog contributes only its parts - a title, a body, and a row of key hints - and stays ignorant of borders, centering, scrolling, and screen size. A Frame supplies that chrome: it caps a dialog to a fraction of the screen and to an absolute maximum, scrolls the body internally when it overflows the cap, and centers the framed box over a backdrop. A Stack owns the ordered dialogs (top is last) plus one Frame, routes every message to the top dialog, and pops it when the dialog reports it is done - handing the popped Dialog back so the caller can read a typed payload off it.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func RenderHints

func RenderHints(keyStyle, textStyle lg.Style, hints []key.Hint) string

RenderHints renders a hint row through primer's inline key renderer, the same "(y)es" styling the rest of a TUI uses; no hints means no row. Shared so a Footered dialog pinning its own hint row renders it identically to the Frame's.

Types

type ClickMsg added in v0.2.1

type ClickMsg struct {
	X, Y int
}

ClickMsg is a left mouse click translated into the top dialog's content space: X is the column and Y the row within the body the dialog rendered through Content, already adjusted for the Frame's frame, title, and scroll offset. The Stack delivers it through Update in place of the raw screen-space click whenever the click lands inside the framed content; clicks on the Frame's chrome or outside the box keep arriving as tea.MouseClickMsg. A click on a Frame-drawn title row arrives with a negative Y, so a dialog checks its own layout before acting.

type Confirm

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

Confirm is a generic confirmation dialog. It renders a prompt as its body and resolves to ResultSubmit when accepted or ResultClose when declined, exposing the decision through Confirmed. It defaults to not confirmed, so a dialog abandoned any other way - escape, or a lost overlay the Stack drops - is read as a decline, the safe default for a guard in front of a destructive action.

Constructed with NewConfirmButtons it renders a focusable button row beneath the prompt: the arrow keys (or h/l, tab) move the focus, enter presses the focused button, each button's Keys press it directly, and a mouse click (delivered as ClickMsg) presses the button it lands on. The plain NewConfirm form keeps the key-only y/n contract and advertises it in the Frame's hint row instead.

It carries pointer semantics: Update mutates and returns the same value, so the caller reads Confirmed off the very pointer the Stack pops.

func NewConfirm

func NewConfirm(prompt string) *Confirm

NewConfirm returns a Confirm that asks prompt, initially not confirmed.

func NewConfirmButtons added in v0.2.1

func NewConfirmButtons(prompt string, buttons ...ConfirmButton) *Confirm

NewConfirmButtons returns a Confirm that asks prompt above a button row built from buttons, in the order given. Focus starts on the first button marked Default (or the first button outright), so the caller decides whether a bare enter lands on the safe choice or the common one.

func (*Confirm) Confirmed

func (c *Confirm) Confirmed() bool

Confirmed reports whether the dialog was accepted. It is meaningful once the Stack has popped the dialog: a Confirm resolved with ResultSubmit reports true, and one that was closed - or is still open - reports false.

func (*Confirm) Content

func (c *Confirm) Content(int) string

Content renders the prompt, with the button row centered beneath it in button mode. The Frame frames, centers, and wraps it, so Confirm is box-less and draws no border of its own.

func (*Confirm) Hints

func (c *Confirm) Hints() []key.Hint

Hints advertises the accept and decline keys for the Frame's foot row. In button mode the buttons are the affordance and the foot row is omitted.

func (*Confirm) Title

func (c *Confirm) Title() string

Title omits a heading; the prompt carries the whole message.

func (*Confirm) Update

func (c *Confirm) Update(msg tea.Msg) (Dialog, tea.Cmd, Result)

Update resolves the dialog on a decision and ignores everything else. In key mode y/Y/enter accept and n/N/esc decline. In button mode enter presses the focused button, the arrows (h/l, tab) move the focus, each button's Keys press it directly, esc declines, and a translated click presses the button under it. The outcome is recorded before returning, so the popped value already carries it.

type ConfirmButton added in v0.2.1

type ConfirmButton struct {
	button.Button

	// Accept makes pressing this button accept the confirmation; a button
	// without it declines.
	Accept bool
	// Keys, when set, are key names (as tea.KeyPressMsg.String() reports them)
	// that press this button directly from the keyboard, e.g. "y" and "Y" for
	// a Yes button. They are optional: a button without Keys is still pressed
	// through focus+enter or a click. Esc always declines regardless of Keys.
	Keys []string
	// Default marks the button the focus starts on. The first button marked
	// Default wins; with none marked, focus starts on the first button.
	Default bool
}

ConfirmButton is one button of a NewConfirmButtons dialog: its rendering plus its meaning. The caller chooses the labels, the order, and the styles - "No/Yes", "Cancel/OK", or anything else - and the semantics travel with each button rather than its position.

func DefaultDestructiveConfirmButtons added in v0.2.3

func DefaultDestructiveConfirmButtons() []ConfirmButton

DefaultDestructiveConfirmButtons returns the standard reject/accept pair for a destructive decision. The active reject uses a red background, the active accept a green background, and accept holds the default focus.

type Dialog

type Dialog interface {
	// Title is the heading the Frame renders above the body; "" omits it.
	Title() string
	// Update routes a message into the dialog. It returns the (possibly
	// updated) dialog so value-typed implementations compose cleanly, a command
	// to run, and the Result telling the Stack whether to keep or pop it.
	Update(msg tea.Msg) (Dialog, tea.Cmd, Result)
	// Content renders the body - and only the body - to fit within width
	// columns. The Frame frames, scrolls, and centers whatever it returns.
	Content(width int) string
	// Hints are the key bindings the Frame renders as the dialog's foot row.
	Hints() []key.Hint
}

Dialog is one overlay's content, free of any chrome. Implementations render their body into the width the Frame allots and never draw their own border, title bar, or scrollbar - the Frame owns all of that. The interface is deliberately minimal: async and busy state belong to the concrete dialog and its own messages, not to this contract.

type Footered

type Footered interface {
	Footer() string
}

Footered is an optional Dialog capability. A dialog that wants a row pinned below its scrollable body - a form's hint/confirm/submitting row that must stay visible however the body scrolls - returns that row here. The Frame scrolls only Content and always renders Footer beneath the viewport (inside the same box). A dialog that does not implement this frames through Content alone, as before. Pairs with ScrollHint, whose region is measured against Content, not the footer.

type Form added in v0.2.1

type Form struct {

	// HoldOnSubmit keeps the form open when it submits: instead of resolving
	// ResultSubmit, the adapter reports EventSubmit through OnEvent and stays
	// put - for owners that freeze the form (SetSubmitting) around an async
	// write and close it themselves with [Stack.Pop] when the write resolves.
	HoldOnSubmit bool
	// contains filtered or unexported fields
}

Form adapts a form.Model into a Dialog: the form's scrollable Body becomes Content, its pinned Foot the Footer, and its FocusRegion the scroll hint, so a tall form follows its focused field inside the Frame while the hint/confirm/submitting row stays visible. EventSubmit resolves to ResultSubmit and EventCancel to ResultClose - the owner reads the values off the popped *Form through Model. Events that keep the form open but need the owner to act - EventEditor, EventChanged, and EventSubmit under HoldOnSubmit - are delivered through the OnEvent callback.

func NewForm added in v0.2.1

func NewForm(m form.Model, onEvent func(form.EventKind), opts ...FormOption) *Form

NewForm wraps m as a dialog. onEvent, when non-nil, observes the events that need the owner to act while the form stays open: EventEditor (take the draft to an external editor), EventChanged (a Notify cycle field stepped - refetch dependent options through Model), and EventSubmit when HoldOnSubmit is set.

func (*Form) Content added in v0.2.1

func (f *Form) Content(int) string

Content is the form's scrollable body; the form is fixed-measure (its Config.Width), so the offered width is ignored.

func (*Form) Footer added in v0.2.1

func (f *Form) Footer() string

Footer pins the form's hint/confirm/submitting row below the viewport.

func (*Form) Hints added in v0.2.1

func (f *Form) Hints() []key.Hint

Hints are omitted; the form's Foot carries its own bindings.

func (*Form) Model added in v0.2.1

func (f *Form) Model() *form.Model

Model returns the wrapped form, for reads and owner-driven mutations: Values after a submit, SetOptions after an EventChanged, SetSubmitting and SetError around an async write.

func (*Form) ScrollTo added in v0.2.1

func (f *Form) ScrollTo() (int, int, bool)

ScrollTo reports the focused field's region so the Frame follows it.

func (*Form) Title added in v0.2.1

func (f *Form) Title() string

Title omits the Frame heading; the form renders its own title in its body.

func (*Form) Update added in v0.2.1

func (f *Form) Update(msg tea.Msg) (Dialog, tea.Cmd, Result)

Update routes a message into the form and maps its event onto the dialog contract.

type FormOption added in v0.2.3

type FormOption func(*Form)

FormOption configures a form dialog created by NewForm.

func WithDiscardButtons added in v0.2.3

func WithDiscardButtons(buttons ...ConfirmButton) FormOption

WithDiscardButtons replaces the default red No / green Yes buttons used by the dirty-discard confirmation. An empty list leaves the defaults intact.

func WithNerdFonts added in v0.2.3

func WithNerdFonts() FormOption

WithNerdFonts renders form-owned buttons with Nerd Font half-circle caps. Callers should enable it only after detecting that the terminal font supports the glyphs.

type Frame added in v0.2.2

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

Frame lays out a dialog: it caps the box to a fraction of the screen and to an absolute maximum, scrolls an overflowing body internally, and centers the result over a backdrop. It carries value semantics and is safe to copy - the Stack holds one by value.

func NewFrame added in v0.2.2

func NewFrame(cfg FrameConfig) Frame

NewFrame builds a Frame from cfg. It constructs the viewport once so RenderScrollable reuses its default key mappings and configuration.

func (*Frame) Render added in v0.2.2

func (s *Frame) Render(backdrop string, d Dialog, screenW, screenH int) string

Render draws d's title, body, and hints into a box, scrolls the composed content when it exceeds the height cap, and composites it centered over backdrop, which must already be screenW columns by screenH rows.

type FrameConfig added in v0.2.2

type FrameConfig struct {
	Styles Styles
	// MaxWidth and MaxHeight cap the box's total size in columns and rows; 0
	// disables the cap. They keep a long prefill or a tall body from bleeding
	// past a large screen.
	MaxWidth  int
	MaxHeight int
	// WidthFraction and HeightFraction cap the box to a share of the screen in
	// (0,1]; 0 disables. They bind on small screens where the absolute caps do
	// not.
	WidthFraction  float64
	HeightFraction float64
	// Margin keeps this many columns and rows clear at the screen edges when
	// the screen, not a cap, is the binding constraint.
	Margin int
	// Scrollbar configures the internal scrollbar's glyphs and geometry.
	Scrollbar scrollbar.Config
}

FrameConfig declares a Frame's chrome and sizing bounds.

type Info added in v0.2.1

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

Info is a message-only dialog: a prompt above a single OK button. It always resolves to ResultClose - there is no decision to carry - on enter, esc, space, or a click on the button; other keys are ignored, so it cannot be dismissed by an accidental keystroke the way a momentary overlay can.

func NewInfo added in v0.2.1

func NewInfo(prompt string, ok button.Button) *Info

NewInfo returns an Info that shows prompt above the injected OK button.

func (*Info) Content added in v0.2.1

func (i *Info) Content(int) string

Content renders the prompt with the OK button centered beneath it.

func (*Info) Hints added in v0.2.1

func (i *Info) Hints() []key.Hint

Hints omits the foot row; the button is the affordance.

func (*Info) Title added in v0.2.1

func (i *Info) Title() string

Title omits a heading; the prompt carries the whole message.

func (*Info) Update added in v0.2.1

func (i *Info) Update(msg tea.Msg) (Dialog, tea.Cmd, Result)

Update closes the dialog on a decision key or a click on the OK button.

type Pick

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

Pick is a filterable single-choice dialog: it wraps a searching list.Model, letting the Frame lay out and scroll a type-to-filter select list. Typing narrows the list and the arrows move the cursor - routed straight into the list - while enter accepts the highlighted row (ResultSubmit) and esc cancels (ResultClose), the two keys the list itself deliberately leaves to its caller. Selection reports the original row index, so the caller keeps a parallel slice of values and maps the index back on submit.

It carries pointer semantics: Update mutates and returns the same value, so the caller reads Selected off the very pointer the Stack pops.

func NewPick

func NewPick(title string, rows []string) *Pick

NewPick returns a Pick over rows, titled title and with the cursor on the first entry. Rows may carry styling; the filter matches their visible text.

func (*Pick) Content

func (p *Pick) Content(int) string

Content renders the list - filter line and matching rows - as the body. The list does not wrap to a width, so the parameter is unused; the Frame lays out and scrolls whatever it returns.

func (*Pick) Hints

func (p *Pick) Hints() []key.Hint

Hints advertises the accept and cancel keys for the Frame's foot row.

func (*Pick) ScrollTo

func (p *Pick) ScrollTo() (int, int, bool)

ScrollTo keeps the highlighted row visible as the cursor moves through a list taller than the box. Implements ScrollHint.

func (*Pick) Selected

func (p *Pick) Selected() (int, bool)

Selected returns the original index of the highlighted row; ok is false when the filter matches nothing (or the list is empty). It is meaningful once the Stack has popped the dialog with ResultSubmit.

func (*Pick) Title

func (p *Pick) Title() string

Title is the heading the Frame renders above the list.

func (*Pick) Update

func (p *Pick) Update(msg tea.Msg) (Dialog, tea.Cmd, Result)

Update resolves the dialog on enter (accept) or esc (cancel) and routes everything else - filter typing, paste, arrow navigation - into the list. Matching on Code, not String, keeps a modified enter or escape working and mirrors the list's own key handling.

type Result

type Result int

Result is what a dialog's Update reports back to the Stack: whether the dialog stays open, or is finished and should be popped. Submit and Close are distinct so a caller reading the popped dialog can tell an accepted result from an abandoned one without inspecting the payload.

const (
	// ResultNone means the dialog consumed (or ignored) the message and stays
	// open.
	ResultNone Result = iota
	// ResultSubmit means the dialog completed successfully; the Stack pops it
	// and returns it so the caller can read a typed payload.
	ResultSubmit
	// ResultClose means the dialog was dismissed without a result; the Stack
	// pops it just the same, and the caller distinguishes the two by Result.
	ResultClose
)

func DismissResult

func DismissResult(msg tea.Msg) Result

DismissResult is the shared Update rule for momentary dialogs (a help sheet, an activity log): the first key press or mouse click closes them, every other message leaves them open. A click inside the framed content arrives as ClickMsg rather than tea.MouseClickMsg, so both count.

type ScrollHint

type ScrollHint interface {
	ScrollTo() (top, height int, ok bool)
}

ScrollHint is an optional Dialog capability. A dialog whose body can grow taller than the Frame's height cap reports the body region it wants kept on screen - a form's focused field, say - so the Frame scrolls to follow it as focus moves rather than stranding the cursor below the fold. top is the region's first body line (0-based, excluding any Frame-drawn title); height is its line span. A dialog that does not implement this, or returns ok false, leaves the viewport at the top (the default before this seam existed).

type SelfFramed

type SelfFramed interface {
	SelfFramed() bool
}

SelfFramed is an optional Dialog capability. A dialog that draws its own complete frame - border, title, and sizing - reports true, and the Stack places its Content verbatim over the backdrop instead of framing and scrolling it through the Frame (which would clip and interleave a pre-drawn box). A dialog that does not implement this, or returns false, gets the Frame's chrome as usual.

type Stack

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

Stack owns an ordered set of modal dialogs - the top is the last element, the one the user interacts with - plus the Frame that renders them. Only the top dialog is live; lower dialogs remain visible underneath it. The zero value is unusable; construct with New.

func New

func New(frame Frame) *Stack

New returns an empty Stack that frames its dialogs with frame.

func (*Stack) Active

func (s *Stack) Active() bool

Active reports whether any dialog is open.

func (*Stack) Pop added in v0.2.1

func (s *Stack) Pop() Dialog

Pop removes and returns the top dialog without a result, or nil when none is open - the seam an owner uses to close a dialog it resolved itself, such as a held-open form (see Form.HoldOnSubmit) whose async write completed. For the grace bookkeeping it counts as a close, so a graced dialog of the same kind reopening right after skips its grace exactly as a keyed close would.

func (*Stack) Push

func (s *Stack) Push(d Dialog)

Push opens d as the new top dialog. A push is user-driven, so it also ends any input grace a previous top was holding.

func (*Stack) PushWithGrace

func (s *Stack) PushWithGrace(d Dialog)

PushWithGrace opens d as the new top dialog behind a brief input grace, for dialogs that open on an async result (a picker pushed when a fetch lands) rather than on a keypress. Keys are absorbed until the keyboard has been quiet for graceQuiet, bounded by graceCeiling from the open; every other message routes normally. When a graced dialog of the same concrete type closed within graceReopenExempt, the grace is skipped - a rapid prompt loop is user-driven by then, and each deliberate key must land.

func (*Stack) ScrollBy added in v0.2.2

func (s *Stack) ScrollBy(delta int) bool

ScrollBy moves the top dialog's internal viewport by delta lines. It returns false when the dialog has not rendered yet or does not overflow its frame.

func (*Stack) ScrollPercent added in v0.2.2

func (s *Stack) ScrollPercent() float64

ScrollPercent reports the top dialog's internal scroll position. A dialog without an active scrollbar reports zero.

func (*Stack) ScrollbarHitbox added in v0.2.1

func (s *Stack) ScrollbarHitbox() (scrollbar.Hitbox, bool)

ScrollbarHitbox returns the screen-space hitbox of the top dialog's internal scrollbar as of the last View, for owners that hit-test mouse presses or drags against it. It reports false when no dialog is open, the body fits without scrolling, or nothing has rendered since the stack last changed.

func (*Stack) SetClock

func (s *Stack) SetClock(now func() time.Time)

SetClock replaces the clock the input-grace windows are measured on - the injected-now seam that lets tests drive the grace deterministically instead of sleeping through it.

func (*Stack) SetFrame added in v0.2.2

func (s *Stack) SetFrame(frame Frame)

SetFrame swaps the frame the Stack draws its dialogs with, letting the owner refresh chrome styles (e.g. after a theme change) without dropping any open dialog.

func (*Stack) SetScrollOffset added in v0.2.2

func (s *Stack) SetScrollOffset(offset int) bool

SetScrollOffset moves the top dialog's internal viewport to offset. It returns false when the dialog has not rendered yet or does not overflow.

func (*Stack) Top

func (s *Stack) Top() Dialog

Top returns the dialog currently receiving input, or nil when none is open.

func (*Stack) Update

func (s *Stack) Update(msg tea.Msg) (tea.Cmd, Dialog, Result)

Update routes a message to the top dialog. When it reports ResultSubmit or ResultClose it is popped and returned, so the caller can type-assert it and read a typed payload; otherwise the returned Dialog is nil. While an input grace is active (see PushWithGrace) key presses are absorbed instead of routed; the first key that arrives after the quiet window - or after the ceiling - ends the grace and drives the dialog.

func (*Stack) View

func (s *Stack) View(backdrop string, screenW, screenH int) string

View composites the top dialog over backdrop, which must already be screenW columns by screenH rows. With no dialog open it returns backdrop unchanged.

type Styles

type Styles struct {
	// Box draws the border and padding around the whole dialog; inject the
	// app's overlay style.
	Box lg.Style
	// Title styles the heading line above the body.
	Title lg.Style
	// HintKey and HintText style the key and description halves of the foot
	// row, matching the rest of the TUI's hint rendering.
	HintKey  lg.Style
	HintText lg.Style
	// Scrollbar styles the internal scrollbar shown when the body overflows
	// the height cap.
	Scrollbar scrollbar.Styles
}

Styles are the Frame's render styles, injected by the owner so the package stays theme-agnostic.

Jump to

Keyboard shortcuts

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