Documentation
¶
Overview ¶
Package action is a screen's verbs: the named things a user can do to whatever is currently selected, declared as data rather than wired up as keypresses.
A screen that has verbs implements Provider, returning a Set of the actions that apply right now. The Menu component renders that Set as a bordered picker anchored where the user asked for it, and resolves to a ChosenMsg — the same shape pkg/confirm and pkg/alert use, so hosting one is the familiar pattern:
func (s *Screen) Update(msg tea.Msg) (screen.Screen, tea.Cmd) {
if s.menuUp {
switch m := msg.(type) {
case action.ChosenMsg:
s.menuUp = false
return s, s.run(m.Action)
case action.CancelledMsg:
s.menuUp = false
return s, nil
}
var cmd tea.Cmd
s.menu, cmd = s.menu.Update(msg)
return s, cmd
}
…
}
Why the verbs are data ¶
Because a footer holds one row and a screen can easily have nine verbs. The alternative — a letter binding per verb — runs out of letters, collides with the vocabulary rule 25 reserves, and puts discovery in a strip that was already truncating. Declaring them lets one surface list them all, and lets the menu answer questions the author would otherwise answer by hand: whether a verb applies to a multi-selection (Multi), whether one is already running (Exclusive), and why a row the user can see is not available (Disabled).
Run and Do ¶
Run is background work: a context, an io.Writer for progress, an error. Do is the escape hatch for verbs that are not background work — pushing a child screen, handing the terminal to $EDITOR. Exactly one may be set; Validate enforces it. This package does not execute either. The Menu reports what was chosen and the host decides, which is what keeps the component testable without a shell, a goroutine, or a subprocess.
Index ¶
- Constants
- func RunKey(a Action, target string) string
- func Validate(s Set) []error
- type Action
- type CancelledMsg
- type ChosenMsg
- type Func
- type Keys
- type Menu
- func (m *Menu) Anchor(x, y int)
- func (m Menu) CanScroll() bool
- func (m *Menu) Center()
- func (m Menu) Cursor() int
- func (m Menu) Help() []key.Binding
- func (m Menu) Init() tea.Cmd
- func (m Menu) Rect() geom.Rect
- func (m Menu) Selected() (Action, bool)
- func (m Menu) Set() Set
- func (m *Menu) SetActions(s Set)
- func (m *Menu) SetCursor(i int)
- func (m *Menu) SetRect(r geom.Rect)
- func (m *Menu) SetRunning(keys map[string]bool)
- func (m Menu) Update(msg tea.Msg) (Menu, tea.Cmd)
- func (m Menu) View() string
- type Options
- type Provider
- type RetargetMsg
- type Set
Constants ¶
const ( DefaultMultiReason = "one item at a time" DefaultRunningReason = "already running" )
Stock reasons the menu fills into Action.Disabled on its own.
Variables ¶
This section is empty.
Functions ¶
func RunKey ¶
RunKey is the identity an Exclusive action is held against: its Ident paired with the target it was launched for.
Pairing with the target is what makes exclusivity useful rather than merely safe — restarting web while api restarts is fine, restarting web twice is not, and an identity that ignored the target could not tell those apart.
func Validate ¶
Validate reports everything structurally wrong with a Set: a missing label, neither or both of Run and Do, a duplicate shortcut, a duplicate identity.
It exists to be called from a test. These are all authoring mistakes whose symptoms show up far from their cause — a duplicate shortcut silently resolves to whichever action is listed first, and a duplicate identity makes one Exclusive action disable an unrelated one — so the useful place to catch them is at build time, not by noticing the menu behaving oddly.
Types ¶
type Action ¶
type Action struct {
// Label names the verb and titles its log event. Required.
Label string
// Desc is an optional gloss rendered beside the label.
Desc string
// Key is an optional shortcut. The menu dispatches it while open and
// renders it in a right-hand column; it is deliberately not advertised
// in the screen's Help(), since moving discovery off the footer and into
// the menu is most of the point.
Key key.Binding
// Confirm, when non-empty, puts a yes/no modal between the pick and the
// run. Use it for anything destructive rather than hand-rolling the
// sequence.
Confirm string
// Disabled, when non-empty, renders the row dimmed and unselectable with
// this text as the reason.
//
// Showing an unavailable verb beats hiding it: hidden, the user learns
// the verb does not exist and goes looking in the docs; shown with a
// reason, they learn it does not apply yet. The menu also fills this in
// on its own — for a non-Multi action under a multi-selection, and for
// an Exclusive action already in flight.
Disabled string
// Multi reports whether this action accepts a selection of more than
// one. The zero value is false: an action acts on exactly one target
// unless it says otherwise.
//
// The default runs the safe way round. "View logs" pushes one screen, so
// forgetting to think about arity leaves a disabled row with an
// explanation — noticed in the first five seconds of using the screen.
// Were the default reversed, forgetting would ship a verb that picks one
// of three marked targets arbitrarily, which is found in production.
Multi bool
// Exclusive refuses a second concurrent run for the same target. The
// menu renders it disabled with a reason rather than dropping the press,
// so the refusal is visible.
Exclusive bool
// ID scopes the Exclusive check. Defaults to Label; set it when two
// screens share a label but not an identity.
ID string
// Run is background work. Prefer it: it can be attributed, grouped,
// cancelled and reported on, none of which Do can be.
Run Func
// Do is the escape hatch for verbs that are not background work.
//
// Navigational Do actions are one-at-a-time by construction rather than
// by declaration — pushing a screen replaces what is on top, so there is
// no second one to push. It is a Do that does not navigate, one that
// fires a request and returns, that may still want Exclusive.
Do func() tea.Cmd
}
Action is one verb over the current selection.
type CancelledMsg ¶
type CancelledMsg struct{}
CancelledMsg reports that the menu was dismissed without a pick.
type ChosenMsg ¶
ChosenMsg reports the action the user picked. The host decides what "chosen" means — run it, confirm it first, push a screen.
type Func ¶
Func is background work launched by an action.
The signature is deliberate on all three counts. ctx makes cancellation non-optional, so an action is no harder to stop than a subprocess. out is an io.Writer rather than a channel or a log closure because it composes with everything that already writes — fmt.Fprintf, io.Copy from a response body, an exec.Cmd's Stdout — where a bespoke callback composes with nothing. The error return is the outcome: an action either worked or it didn't, and ctx.Err() reading as a failure is correct, because the user stopped it.
type Keys ¶
type Keys struct {
Up, Down key.Binding
Top, Bottom key.Binding
HalfUp, HalfDown key.Binding
Choose key.Binding
Cancel key.Binding
}
Keys is the menu's keymap. Vertical movement follows rule 25 exactly — a menu is one more thing that scrolls, not a place to invent a vocabulary.
func (*Keys) FillDefaults ¶
func (k *Keys) FillDefaults()
FillDefaults fills any zero-valued binding with its DefaultKeys counterpart, so partial overrides work without restating every field.
type Menu ¶
type Menu struct {
// contains filtered or unexported fields
}
Menu is the action picker: a bordered list of verbs that sizes itself to its content and places itself where it was asked to.
It is hosted the way pkg/confirm and pkg/alert are — a host owns show/hide, forwards every message while it is up, and matches ChosenMsg / CancelledMsg in its own Update. Composition is a bare layout.Sized inside a ZStack: the menu treats the rect it is given as outer bounds and draws itself inside them, so there is no layout.Center wrapper to keep in sync with its size.
func (*Menu) Anchor ¶
Anchor places the menu's top-left at (x, y) — where a right-click landed — clamped so it stays on screen. Call before the frame that shows it.
func (*Menu) Center ¶
func (m *Menu) Center()
Center places the menu in the middle of the bounds it is given — the keyboard path, where there is no pointer to place it under.
A top-anchored variant was tried and reverted: it sat closer to the rows on a tall terminal, but it covered the pane's filter row and its rule, which reads as a broken frame. Centering leaves the pane's chrome intact, and a modal in the middle of the pane is where a modal is expected to be.
func (Menu) Help ¶
Help returns the bindings the menu currently responds to, including the mouse affordance as a sentinel binding (rule 10) so it can be advertised in the expanded panel without ever matching a real key.
func (*Menu) SetActions ¶
SetActions swaps the offered actions and resets the cursor to the first selectable row.
func (*Menu) SetCursor ¶
SetCursor moves the highlight, skipping to the next selectable row when the requested one is disabled. Carries state across a theme rebuild (rule 4).
func (*Menu) SetRect ¶
SetRect treats r as outer bounds: the menu measures its rows, sizes itself against caps derived from r, and places itself inside it.
Same contract as an autosize alert, and for the same reason — a picker whose size depends on its content cannot be given a size by the layout engine without the caller duplicating the measurement.
func (*Menu) SetRunning ¶
SetRunning tells the menu which actions are in flight, so Exclusive ones render as unavailable with a reason instead of silently declining the press. Keys come from RunKey.
The menu is told rather than asked because it has no view of the run registry — whoever launched the work does. That keeps this component free of any dependency on how actions are executed, which is what lets it be tested without a goroutine in sight.
type Options ¶
type Options struct {
// Title labels the menu. Defaults to "Actions"; the Set's Target is
// appended when it has one, so a menu over a multi-selection announces
// its blast radius on its own border.
Title string
// Set is the actions to offer.
Set Set
LabelStyle lipgloss.Style
DescStyle lipgloss.Style
KeyStyle lipgloss.Style
SelectedStyle lipgloss.Style
DisabledStyle lipgloss.Style
ActiveColor lipgloss.TerminalColor
InactiveColor lipgloss.TerminalColor
ActiveBorder lipgloss.Border
InactiveBorder lipgloss.Border
SlotBrackets pane.SlotBracketStyle
// MultiReason and RunningReason override the stock text the menu fills
// into Disabled for a non-Multi action under a multi-selection, and for
// an Exclusive action already in flight.
MultiReason string
RunningReason string
Keys Keys
}
Options configures a Menu. Build it from theme.Actions() and set Title and Set before passing to New.
type Provider ¶
type Provider interface {
Actions() Set
}
Provider is implemented by screens that have verbs.
It is an optional interface rather than a method on screen.Screen so every existing screen keeps compiling and simply has no actions, which is the honest state of affairs for a screen that has not declared any.
Implementations are called on menu open and on key dispatch, so the same contract applies as to Help(): cheap, allocation-light, no I/O.
type RetargetMsg ¶
type RetargetMsg struct {
// Event is the press, forwarded untouched so the host can route it to
// whatever is underneath before it reopens.
Event mouse.Msg
}
RetargetMsg reports a right-press that landed outside the open menu — the gesture meaning "ask me about this one instead."
The menu emits it rather than acting, because retargeting needs to know what is under the pointer in the layer beneath and the menu cannot see there. A host that handles it moves its own selection to the event and reopens; a host that ignores it leaves the menu exactly as it was, which is why adding this could not change any existing behaviour.
type Set ¶
type Set struct {
// Target names the object of the verbs for display — "cache-redis",
// "3 items". It titles the menu.
//
// It is the whole reason Set is a struct rather than a bare slice: once
// rows can be marked, "Delete" means one row or twelve, and the menu is
// the last surface that can say which before it happens.
Target string
// Count is how many targets Actions will act on. 0 and 1 both mean a
// single target; above that, actions without Multi are disabled.
Count int
Actions []Action
}
Set is a screen's actions plus what they will act on.