Documentation
¶
Overview ¶
Package activity is per-row in-flight state: the spinner and status label a table cell, list row or tree node shows while work against that specific row is running.
It is the row-scale counterpart to pane.Pane's loading state. SetLoading means "this whole component has no data yet" and replaces the body; activity means "two of these forty rows are busy and the other thirty-eight are still true", which the pane has no way to say.
Held by key ¶
Every entry is keyed the way marks are — by the key from SetKeyedRows / SetKeyedItems, or a tree node's path — and for the same reason, only more so. The whole premise of showing a spinner is that something is changing the data underneath, so a polled refresh reordering the rows mid-flight is the expected case rather than the unlucky one. An indicator held by index would drift onto a neighbour exactly when the user is watching it.
Never pre-styled ¶
Render returns text a caller colors itself, through Options.Style. This package holds no lipgloss import and emits no escapes, for the reason pkg/glyph gives: a table cell needs a foreground-only escape or it punches a hole in the selected row's background (CLAUDE.md rule 19), while a list row can use lipgloss freely. Only the component knows which it is.
Ownership ¶
A Set holds its own spinner and drives its own tick chain, so a component embedding one gets the animation without a screen having to re-push rows every frame. Setters return a tea.Cmd the caller must propagate, exactly as pane.SetLoading does (rule 17). The chain runs while any key is running and stops when the last one finishes, so an idle component schedules nothing.
Index ¶
- Constants
- func Busy(values ...string) func(value string) (label string, busy bool)
- func Progress(out io.Writer, text string)
- func Settled(values ...string) func(value string) (label string, busy bool)
- type Change
- type EndMsg
- type Options
- type Progresser
- type Set
- func (s Set) Active() bool
- func (s *Set) Adopt(other Set) tea.Cmd
- func (s Set) Badge(key, row string, width int) string
- func (s *Set) Clear(key string)
- func (s *Set) ClearAll()
- func (s Set) Count() int
- func (s *Set) Derive(busy map[string]string) tea.Cmd
- func (s *Set) Finish(key string, err error) tea.Cmd
- func (s *Set) FinishRun(runID int64, err error) tea.Cmd
- func (s *Set) Handle(msg tea.Msg, holds func(key string) bool) tea.Cmd
- func (s *Set) Relabel(key, label string)
- func (s *Set) RelabelRun(runID int64, label string)
- func (s Set) Render(key string, width int) (string, bool)
- func (s *Set) Revise(changes map[string]Change) tea.Cmd
- func (s *Set) Start(key, label string) tea.Cmd
- func (s *Set) StartRun(keys []string, label string, runID int64) tea.Cmd
- func (s Set) State(key string) (State, bool)
- func (s Set) Width(key string, width int) int
- type StartMsg
- type State
- type UpdateMsg
Constants ¶
const DefaultConfirm = 30 * time.Second
DefaultConfirm bounds decision 19's handoff: how long an entry whose action succeeded keeps the row moving while it waits for the data to catch up.
It is not a guess about how long the work takes — the handoff normally ends at the next observation, whenever that is. It is a bound on how long the library will wait to be told anything at all, for the screen whose polling is paused, whose tab nobody is looking at, or whose endpoint is down.
const DefaultHold = 2 * time.Second
DefaultHold is how long a finished key shows its outcome before clearing.
A spinner that simply vanishes leaves no evidence of what happened, and the statusbar receipt is one line for a whole run and wiped by the next keypress (rule 20). When six rows are working and one fails, the per-row outcome is the only surface that says which.
Variables ¶
This section is empty.
Functions ¶
func Busy ¶
Busy builds the ordinary ActivityWhen predicate: a case-insensitive match against the values that mean "in progress", labelled with the value as it actually appeared.
activity.Busy("running", "pending", "waiting")
Values are compared with surrounding space and any ANSI styling stripped, so a cell coloured by the screen still matches.
func Progress ¶
Progress updates the label on the rows the running action is acting on.
It rides the io.Writer an action already holds rather than adding a second channel back to the UI, the way http.Flusher extends http.ResponseWriter. A no-op when out does not support it, so an action written against a plain io.Writer keeps working:
Run: func(ctx context.Context, out io.Writer) error {
fmt.Fprintln(out, "sync started") // the console
activity.Progress(out, "syncing 3/7") // the rows
…
}
Progress is deliberately not logged. It is a UI state change, not news: a run reporting progress ten times would otherwise post ten records into an event the console badge counts as one.
func Settled ¶
Settled is Busy inverted: it names the values that mean nothing is happening, and reports everything else as in flight, labelled with the value as it appeared.
activity.Settled("successful", "failed", "canceled")
Prefer it when the API documents a set of *terminal* states, which is how most of them are written — and because it keeps working when the server learns a new in-progress status. A list of busy values would quietly treat that new one as settled and stop spinning for it; this treats it as work, which is the safer way to be wrong.
The trade is the mirror image: a new *settled* status this does not know about spins forever. Pick the list the server is less likely to extend, and remember the row only ever reports what it was told.
An empty value is settled: a blank cell is not work in progress.
Types ¶
type Change ¶
type Change struct {
// Rev is a value that changes whenever the row's underlying work does —
// finished_at, resourceVersion, an ETag, the id of the latest run. Its
// content is never interpreted; only whether it differs from last time.
Rev string
// Label is what the flash shows. Usually the row's own new value, because
// in a table the indicator replaces the cell — a bare glyph there would
// hide the very change it is pointing at.
Label string
}
Change is one key's revision plus what a flash should say about it.
type Options ¶
type Options struct {
// Spinner is the frame set. Nil means spinner.Dot, matching pane.
Spinner *spinner.Spinner
// Glyphs supplies ActivityOK / ActivityFail. Empty fields resolve to
// glyph.Default.
Glyphs glyph.Set
// Hold is how long a finished key shows its outcome. Zero means
// DefaultHold; negative means hold indefinitely, for a component whose
// data will not refresh on its own and whose owner will call Clear.
Hold time.Duration
// Confirm caps how long a locally-started entry waits for the data to
// confirm it finished, before falling back to reporting its own outcome.
// Zero means DefaultConfirm. Only consulted once Derive has been called
// at least once — with no source of truth there is nothing to wait for.
Confirm time.Duration
// Style colors the rendered indicator.
//
// A function rather than a lipgloss.Style because a table cell needs a
// foreground-only escape (rule 19) and a list row does not — and only the
// component knows which it is. The theme builders supply the right form
// per component. Nil leaves the text plain.
Style func(st State, text string) string
}
Options configures a Set.
type Progresser ¶
type Progresser interface {
Progress(text string)
}
Progresser is implemented by a writer that can report a label change back to the rows an action is running against. runner's capture writer satisfies it.
type Set ¶
type Set struct {
// contains filtered or unexported fields
}
Set is the keyed collection of in-flight rows plus the spinner that animates them. Components embed one.
Copies share their entries, the same way a component's marks map does: a value-receiver Update that hands back a copy still sees writes made through a pointer elsewhere. The spinner and the ticking flag are value state, so they travel with whichever copy the component stores.
func (Set) Active ¶
Active reports whether any key has an entry — running, holding an outcome, or derived from the data.
func (*Set) Adopt ¶
Adopt takes over another Set's entries, keeping this Set's own options.
This is the rule-4 primitive: a theme swap rebuilds the component, so the new Set carries the new palette and the old work. The returned command re-arms the spinner and any hold that was mid-flight, or a swap during those two seconds would strand an outcome glyph on the row forever.
func (Set) Badge ¶
Badge right-aligns key's indicator at the end of a row width cells wide.
The row is what gives way when the two cannot both fit: the badge is the news, and a row label the user can already read most of loses less by being cut than the indicator does by disappearing. The indicator is capped at half the width so a long label cannot swallow the row entirely.
Returns row untouched when the key has no entry, so a caller can compose unconditionally. Row may already carry escapes — widths are measured and cuts made ANSI-aware.
func (*Set) Clear ¶
Clear retires one key immediately, outcome or not. Both layers: a derived entry left behind would keep the row spinning until the next observation, which is not what "clear this" means.
func (*Set) Derive ¶
Derive replaces the derived layer from one observation of the data.
busy maps the keys that are in flight to what they should say; every key absent from it is not in flight. Wholesale rather than incremental because an observation *is* the whole truth as of that moment — an incremental API would make "this row stopped being busy" something the caller has to notice and report, which is the bookkeeping this is meant to remove.
A key's Since survives across observations, so elapsed time measures the work rather than the poll that last saw it.
Calling this is also the observation decision 19 waits for: it retires any local entry whose action finished successfully since the last one. That is why it must be called on every swap, including one where nothing is busy — an empty map is a real observation and the only thing that can end a handoff.
func (*Set) Handle ¶
Handle applies a broadcast or a spinner tick, and returns any command that keeps the animation running.
holds reports whether this component owns a key; a nil holds accepts every key in a StartMsg. Keys the component does not hold are ignored, which is the same decline-what-isn't-yours behaviour components already perform for mouse events outside their rect (rule 28).
func (*Set) Relabel ¶
Relabel changes what one key says without restarting it, so Since keeps measuring the work rather than the last progress report. Local layer only: a derived label is a fact about the data and is replaced by the next observation, not edited.
func (*Set) RelabelRun ¶
RelabelRun changes what every key of a run says.
func (Set) Render ¶
Render is the indicator for one key, fitted to width and styled.
Below the width the label needs, the glyph alone is drawn: a cell of eight showing "⠹ syncin" is worse than one showing "⠹". Reports false when the key has no entry, so a caller can fall through to whatever the row normally shows.
func (*Set) Revise ¶
Revise records revisions and flashes the keys whose work changed unseen.
A key whose Rev differs from the last observation, and which is neither busy nor locally owned, gets a brief Changed outcome: the row's work moved and this session never saw it running (decision 20). A key seen for the first time never flashes, or every row would flash on the first load.
Nor does a key the last observation reported busy, or one whose handoff this observation just retired. Those changes were on screen as a spinner from start to finish, and a flash after them says the same thing twice — the flash exists for work that was never visible at all.
Call it after Derive, which is what decides whether a key counts as busy.
type StartMsg ¶
StartMsg begins activity on Keys. Broadcast by the app shell when a run it launched reports itself started; a component applies only the keys it holds.
type State ¶
type State struct {
// Label is what the row says — "syncing", "syncing 3/7".
Label string
// RunID is the run that owns this entry, for the broadcasts that address
// a whole run at once. Zero when the entry was set directly.
RunID int64
// Since is when the work started, not when the label last changed.
Since time.Time
// Done reports that the work finished and the entry is showing its
// outcome before it clears.
Done bool
// Err is the outcome, non-nil on failure. Meaningless unless Done.
Err error
// Changed reports that this entry is a revision flash — the row's work
// changed somewhere the TUI never saw running (decision 20) — rather than
// the outcome of something it watched. Meaningless unless Done.
Changed bool
}
State is one key's in-flight state.