action

package
v0.21.0 Latest Latest
Warning

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

Go to latest
Published: Aug 26, 2026 License: MIT Imports: 11 Imported by: 0

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

View Source
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

func RunKey(a Action, target string) string

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

func Validate(s Set) []error

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.

func (Action) Ident

func (a Action) Ident() string

Ident is the action's identity for the Exclusive check. Defaults to Label.

type CancelledMsg

type CancelledMsg struct{}

CancelledMsg reports that the menu was dismissed without a pick.

type ChosenMsg

type ChosenMsg struct {
	Action Action
	Target string
}

ChosenMsg reports the action the user picked. The host decides what "chosen" means — run it, confirm it first, push a screen.

type Func

type Func func(ctx context.Context, out io.Writer) error

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 DefaultKeys

func DefaultKeys() Keys

DefaultKeys returns the menu's stock keymap.

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 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 New

func New(opts Options) Menu

New constructs a menu.

func (m *Menu) Anchor(x, y int)

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 (m Menu) CanScroll() bool

CanScroll reports whether the actions overflow the box.

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 (m Menu) Cursor() int

Cursor is the highlighted row, or -1 when nothing is selectable.

func (m Menu) Help() []key.Binding

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 (m Menu) Init() tea.Cmd

Init satisfies tea.Model — nothing to kick off.

func (m Menu) Rect() geom.Rect

Rect is where the box actually landed, for a host that needs to know.

func (m Menu) Selected() (Action, bool)

Selected returns the highlighted action.

func (m Menu) Set() Set

Set returns the actions currently offered.

func (m *Menu) SetActions(s Set)

SetActions swaps the offered actions and resets the cursor to the first selectable row.

func (m *Menu) SetCursor(i int)

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 (m *Menu) SetRect(r geom.Rect)

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 (m *Menu) SetRunning(keys map[string]bool)

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.

func (m Menu) Update(msg tea.Msg) (Menu, tea.Cmd)

Update routes keys and mouse. The host forwards every message while the menu is up and none while it is down (the ZStack occlusion contract).

func (m Menu) View() string

View renders the box padded into the outer bounds it was given, so the host composes with layout.Sized alone. Rows the box does not cover are emitted empty, which ZStack passes straight through to the base layer.

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.

func (Set) Empty

func (s Set) Empty() bool

Empty reports whether the set has no actions.

Jump to

Keyboard shortcuts

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