Documentation
¶
Overview ¶
Package form is the modal text-input controller for overlay forms: an ordered set of single- and multi-line fields with one focus ring, a dirty-discard guard (esc on edited content asks yes/no instead of dropping it), a submit/cancel/editor event contract with a consumed-key signal, and a pluggable autocomplete seam (a trigger rune such as @ starts a query; suggestions are fetched asynchronously as commands). The package is domain-free — it knows nothing about issues or Jira — so any overlay that collects text can ride it, and it stays liftable into a standalone module. Following primer's idiom it is state structs plus render helpers, not a tea.Model: the owner routes messages in and places the rendered content. It is deliberately not primer's input.Editor: Editor is a full tea.Model over a fixed title+body pair, where this package mixes one-line and multiline fields, gates required ones, and exposes the completion seam.
Index ¶
- type Autocomplete
- type Config
- type EventKind
- type FieldSpec
- type Model
- func (m *Model) AcceptedDetail(i int) string
- func (m *Model) Active() bool
- func (m *Model) Body() string
- func (m *Model) FocusRegion() (top, height int, ok bool)
- func (m *Model) Foot() string
- func (m *Model) SetError(msg string)
- func (m *Model) SetOptions(i int, options []string)
- func (m *Model) SetSubmitting(frame string)
- func (m *Model) SetValue(i int, s string)
- func (m *Model) Update(msg tea.Msg) (tea.Cmd, EventKind, bool)
- func (m *Model) Value(i int) string
- func (m *Model) Values() []string
- func (m *Model) View() string
- type Styles
- type Suggestion
- type SuggestionsMsg
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Autocomplete ¶
type Autocomplete struct {
// Trigger starts a completion token at a word boundary, e.g. '@'. Zero
// means bare mode: every trailing token completes and an acceptance
// replaces the token as-is — for value lists rather than mentions.
Trigger rune
// MinQuery is how many runes must follow the trigger before fetching;
// zero means 1, so a bare trigger never fires a fetch.
MinQuery int
// IsBoundary marks the runes that end a token; nil means whitespace.
// A comma-separated field adds ',' so each list entry completes alone.
IsBoundary func(r rune) bool
// Fetch resolves a query to suggestions. It runs inside a tea.Cmd (its
// own goroutine), so it may block on I/O; the result is dropped if the
// query has moved on by the time it lands.
Fetch func(query string) []Suggestion
}
Autocomplete plugs completion into one field. The form watches the word being typed at the cursor; once it starts with Trigger and the query is long enough, Fetch runs in a command and its results show as a list.
type Config ¶
type Config struct {
// Title renders above the fields (e.g. "comment on PROJ-1").
Title string
Fields []FieldSpec
// EditorHatch offers ctrl+e on a focused multiline field: the form emits
// EventEditor and the owner takes the draft to an external editor. The
// in-TUI field is always the default; the editor is the escape hatch.
EditorHatch bool
// Width bounds every field's rendered width.
Width int
Styles Styles
}
Config declares a whole form.
type EventKind ¶
type EventKind int
EventKind is what a completed Update asks the owner to do.
const ( // EventNone means the form consumed (or ignored) the message and stays open. EventNone EventKind = iota // EventSubmit means every required field is filled; read Values. EventSubmit // EventCancel means the user backed out (esc on a pristine form, or a // confirmed discard). The values are abandoned. EventCancel // EventEditor asks the owner to continue the focused multiline field in // the external editor, seeded with Values. The form stays open until the // owner closes it, so a failed editor launch loses nothing. EventEditor // EventChanged means a Notify cycle field just stepped to a new value; the // owner reads it (via Value) and may push dependent Options back through // SetOptions. The form stays open. EventChanged )
type FieldSpec ¶
type FieldSpec struct {
// Label renders above the field; empty omits the row.
Label string
// Placeholder shows in the empty field.
Placeholder string
// Initial pre-fills the field; the dirty guard compares against it, so a
// prefilled-then-untouched form still cancels without a confirmation.
Initial string
// Multiline makes the field a textarea (enter inserts a newline and
// ctrl+s submits) instead of a one-line input.
Multiline bool
// Rows is the textarea height; zero means 5.
Rows int
// Options, when non-empty, makes the field a cycle selector rather than a
// text input: it holds one of the listed values and ←/→ (or h/l) step
// through them. Its value is always one of Options, so a required cycle
// field can never be blank. Initial selects the starting option (the first
// when Initial matches none). Multiline/Autocomplete are ignored on a
// cycle field.
Options []string
// Notify marks a cycle field whose value changes the owner wants to react
// to — a project selector that drives a dependent type list, say. When such
// a field steps to a new value the form returns EventChanged, so the owner
// can refetch and push new Options back through SetOptions.
Notify bool
// Optional lets the field submit blank. A required field left blank
// blocks the submit and takes focus instead. An Optional field renders an
// "(optional)" marker on its label so the blank-is-fine contract is visible.
Optional bool
// Validate, when set, runs on trySubmit against the field's value. A
// non-nil error blocks the submit, focuses the field, and renders the
// message inline beneath it — the same gate the required check uses, but
// for content the field itself can't express (a duration that won't parse).
Validate func(string) error
// Autocomplete, when set, watches this field for its trigger token and
// offers fetched suggestions. Nil disables the seam.
Autocomplete *Autocomplete
}
FieldSpec declares one field of a form.
type Model ¶
type Model struct {
// contains filtered or unexported fields
}
Model is one open form. The zero value is inert; construct with New.
func (*Model) AcceptedDetail ¶ added in v0.12.1
AcceptedDetail returns the Detail of the suggestion last accepted into field i (empty when none was accepted, or a later edit invalidated it). The owner uses it to recover the opaque value behind a display label — an assignee's accountId, say — without re-resolving the field text.
func (*Model) Body ¶ added in v0.12.1
Body is the scrollable part of the form: the title and the labeled fields (with any suggestion list under the focused one). FocusRegion is measured against exactly this, so an owner can scroll Body to follow focus and pin Foot beneath the viewport.
func (*Model) FocusRegion ¶ added in v0.12.1
FocusRegion reports the focused field's block position within View(): top is its first line (0-based), height its line span. A framing Shell scrolls to keep that range visible as focus moves, so a form taller than the height cap follows the cursor instead of stranding it off-screen. ok is false for an inert form. Lines are counted by newline exactly as View joins the blocks, so the region lands on the same rows the Shell renders.
func (*Model) Foot ¶ added in v0.12.1
Foot is the form's pinned chrome: any form-level error above the hint (or submitting, or discard-confirm) row. It must stay visible however the body scrolls — it carries the submit/cancel/confirm affordances — so an owner renders it outside the scroll viewport.
func (*Model) SetError ¶ added in v0.12.1
SetError clears the submitting state and shows msg as a form-level error line above the hint row — the seam an owner uses to surface a failed submit while keeping the draft intact.
func (*Model) SetOptions ¶ added in v0.12.1
SetOptions replaces cycle field i's options — the seam an owner uses to swap a dependent list live (the type list that changes with the selected project). The selection stays on its current value when that value survives the swap, otherwise it snaps to the first option so the field never points past the end. A no-op on a non-cycle field or an empty list (which would degrade the field to a blank). initialSel tracks the new selection, so an owner-driven swap does not by itself read as a user edit — the field that drove the change is what marks the form dirty.
func (*Model) SetSubmitting ¶ added in v0.12.1
SetSubmitting marks the form as awaiting an async submit, rendering frame (a spinner glyph) plus "submitting…" in place of the hint row. It clears any prior form-level error so the two never show at once.
func (*Model) SetValue ¶
SetValue replaces field i's content without touching its Initial, so the dirty guard keeps comparing against the true baseline — the seam a caller rebuilding a form mid-edit (a resize) needs to carry a draft over.
func (*Model) Update ¶
Update routes a message into the form. The event tells the owner what completed; consumed reports whether the form owned the message, so an owner layering global keys knows when to fall through.
func (*Model) View ¶
View renders the whole form — the scrollable body then the pinned foot — as one string. It is the plain path (tests, any owner that frames the form itself); an owner that scrolls a tall form draws Body and Foot separately so the foot row never scrolls off (see Model.Body, Model.Foot).
type Styles ¶
type Styles struct {
Title lipgloss.Style
Label lipgloss.Style
LabelFocused lipgloss.Style
Border lipgloss.Style // an unfocused field's box border and cycle chevrons
BorderFocused lipgloss.Style // the focused field's box border and cycle chevrons
HintKey lipgloss.Style
HintText lipgloss.Style
Question lipgloss.Style // the dirty-discard confirmation
Suggestion lipgloss.Style
SuggestionSelected lipgloss.Style
Error lipgloss.Style // field and form-level validation/submit errors
}
Styles are the form's render styles, injected by the owner so the form stays theme-agnostic.
type Suggestion ¶ added in v0.12.1
type Suggestion struct {
Value string
Label string
// Detail is an opaque payload carried with the suggestion but never
// inserted into the field — the owner reads it via AcceptedDetail after an
// acceptance. It lets a readable label (an assignee's display name) stand
// in the field while the owner keeps the value it maps to (an accountId),
// so no re-resolve is needed at submit.
Detail string
}
Suggestion is one completion candidate: Value is inserted into the field on acceptance (with the trigger rune re-prefixed when there is one), while Label is what the list renders. The two are equal for plain value lists; they diverge when a human-readable label stands in for an opaque value.
type SuggestionsMsg ¶
type SuggestionsMsg struct {
Field int
Query string
Items []Suggestion
}
SuggestionsMsg carries fetched suggestions back into the form. Field and Query pin the result to the fetch that asked, so a slow response can never attach to a newer token (the generation-guard idea, keyed by content).