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.
The model ¶
The server is the source of truth. When the user starts work locally, the row is told at once, and that claim lasts until the truth can take over.
A component observes its own rows on every keyed swap, a predicate says which of them are working, and those rows spin. Every observation replaces the collection outright; nothing accumulates.
Work the user starts is an operation (op.go): Dispatch opens a claim, Done reports that the request answered, and the Mode says who knows when the work ends — the server's status (Observed) or the request itself (Held). BeginRead and Accept order reads against those answers, so a read taken across a write cannot end its claim and a failed read withdraws what was waiting on one. Screens reach all of this through their component — table.Dispatch, table.ApplyRead, and the same on list and tree — and rarely touch a Set.
What remains outside all of this, as a design choice rather than an oversight: work that somebody else began and ended between two observations is never seen.
Held by key ¶
Every entry is keyed by the key from SetKeyedRows / SetKeyedItems, or a tree node's path — the same key marking uses, and for a sharper reason. 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 the caller colors itself, through Options.Style. This package emits no escapes, because a table cell needs a foreground-only one 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.
Ownership ¶
A Set holds its own spinner and drives its own tick chain, so a component embedding one gets the animation without a screen re-pushing rows every frame. Derive returns a tea.Cmd the caller must propagate, exactly as pane.SetLoading does (rule 17). The chain runs while any key is busy and stops when the last one settles, so an idle component schedules nothing.
Index ¶
- func Busy(values ...string) func(value string) (label string, busy bool)
- func Settled(values ...string) func(value string) (label string, busy bool)
- type DispatchOptions
- type Mode
- type Op
- type Options
- type Outcome
- type Read
- type Set
- func (s *Set) Accept(rd Read, err error) bool
- 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) BeginRead() Read
- func (s Set) Count() int
- func (s *Set) Dispatch(keys []string, label string, mode Mode) (Op, tea.Cmd)
- func (s *Set) DispatchWith(keys []string, label string, mode Mode, opts DispatchOptions) (Op, tea.Cmd)
- func (s *Set) Done(op Op, err error) Outcome
- func (s *Set) Handle(msg tea.Msg) tea.Cmd
- func (s *Set) ObserveRows(keys []string, eval func(i int) (label string, busy bool, rev string)) tea.Cmd
- func (s Set) ReadsFailing() bool
- func (s Set) Render(key string, width int) (string, bool)
- func (s Set) State(key string) (State, bool)
- type State
- type UnobservedMsg
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Busy ¶
Busy builds the ordinary predicate: a case-insensitive match against the values that mean "in progress", labelled with the value as it 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.
The value comes back plain whether or not it matched: under a component's BusyWhen it is the row's status, which an operation compares against to notice the server acted, so a settled row has to say what it settled at.
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, where a list of busy values would quietly treat that new one as settled and stop spinning. 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.
An empty value is settled: a blank cell is not work in progress.
Types ¶
type DispatchOptions ¶ added in v0.35.0
type DispatchOptions struct {
// Settle overrides Options.Settle for this operation's claims. Nil keeps
// the Set's value.
//
// Verbs on one component can face different servers: a sync handed to a
// reconciler may need two reads before the work shows, while an inline
// operation needs none. One value per Set would make the fast verb's
// claim linger through reads that already showed nothing happened.
Settle *int
}
DispatchOptions adjusts one Dispatch away from the Set's defaults.
type Mode ¶ added in v0.30.0
type Mode int
Mode is who knows when an operation's work ends.
const ( // Observed: the server's status does. The request is only an // acknowledgement — Argo's sync POST answers at once and the controller // does the work — so the claim covers the gap until a read reports the // key busy, and ends when reads taken after the acknowledgement say the // work is over or run out of allowance (Options.Settle). Observed Mode = iota // Held: the request does. Argo's refresh GET holds the connection until // the refresh is done and shows nothing on the app while it runs; a job // handle is the same, with the screen calling Done when the job resource // says it finished. Observations cannot end a Held claim — an open // request is the server not having answered yet — though a read that // reports the key busy still shows the server's own label. Held )
type Op ¶ added in v0.30.0
Op is one dispatched operation. It is a small value meant to ride the screen's own messages from Dispatch to Done.
type Options ¶
type Options struct {
// Spinner is the frame set. Nil means spinner.Dot, matching pane.
Spinner *spinner.Spinner
// 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
// Settle is how many observations that say nothing new an unconfirmed
// claim survives — an Observed Dispatch, or an Expect.
//
// Counted in observations rather than seconds, because no duration the
// client can measure is the right length of that wait: it is set by the
// server's reconcile lag, which is neither published nor stable. An
// observation is spent only when it repeats the value the key had when the
// claim was made; one that reports the key busy confirms the claim
// instead, and one that reports a different settled value retires it
// outright, whichever way the allowance stands.
//
// Zero — the default, and correct for any API whose handler sets the
// status inline — retires a claim on the next observation. A reconciler
// wants one or two, not a number tuned to its control loop.
Settle int
}
Options configures a Set.
type Outcome ¶ added in v0.30.0
type Outcome int
Outcome is what Done did to an operation — and so what the screen may tell the user when the reply arrives.
Done means different things by Mode, which is why it says which: after an Observed request answers, the work has only been accepted and is still running; after a Held one, it is over. A screen with both kinds of verb that writes "completed" on every reply is wrong for half of them, and nothing else would tell it so.
const ( // Acknowledged: an Observed request answered. The server has the work and // the row keeps spinning until reads say it finished. Say "requested", // never "completed". Acknowledged Outcome = iota // Ended: a Held request answered, so the work is over. "Completed" is // true now. Ended // Withdrawn: the request failed, so nothing is coming and the claim is // gone. Report the error. Withdrawn )
type Read ¶ added in v0.30.0
type Read struct {
// contains filtered or unexported fields
}
Read is a token for one request/response read, taken when it is issued.
type Set ¶
type Set struct {
// contains filtered or unexported fields
}
Set is the keyed collection of busy rows plus the spinner that animates them. Components embed one.
It is a value, and every mutating method takes a pointer: ObserveRows replaces the collection outright rather than editing it, so a copy taken beforehand keeps the observation it was made with. A component therefore has to keep the Set it mutated — which is what bubbletea's value-receiver Update already does by returning the model.
func (*Set) Accept ¶ added in v0.30.0
Accept decides whether a read that has landed may be applied, and must be called before its rows are pushed.
It refuses a read overtaken by a newer one already applied. It refuses a failed read too, and withdraws every acknowledged Observed claim when it does: those are waiting on an observation, and a read that failed is the case where none is coming. Held claims and claims whose request is still in flight are left alone, since their own request will end them.
On true, the next observation is taken as of rd. On a failure, rows busy only because an earlier read said so turn Unknown until the next observation.
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 observation. Without it a swap would blank every indicator until the next poll.
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 is not busy, so a caller can compose unconditionally. Row may already carry escapes — widths are measured and cuts made ANSI-aware.
func (*Set) BeginRead ¶ added in v0.30.0
BeginRead stamps a read at the moment it is issued.
What a read can say is fixed when it is issued, not when it lands, so this is the point that is compared against acknowledgements and against other reads. A stream has no such point — its events are taken as they arrive, in order — and needs no token.
func (Set) Count ¶
Count is how many of the component's keys are working — observed or claimed.
Scoped, so it answers "how many of my rows" rather than "how many entries do I hold": a claim on a row a refresh removed would otherwise animate a spinner nothing can see and inflate a counter a screen might put in a title.
func (*Set) Dispatch ¶ added in v0.30.0
Dispatch opens a claim on keys for work the screen is about to request.
Each key's status as of the last observation is recorded with the claim; a later read reporting a different one is how the Set notices the server acted. The returned command is the animation's first tick.
func (*Set) DispatchWith ¶ added in v0.35.0
func (s *Set) DispatchWith(keys []string, label string, mode Mode, opts DispatchOptions) (Op, tea.Cmd)
DispatchWith is Dispatch with per-operation options.
func (*Set) Done ¶ added in v0.30.0
Done reports that op's request answered, and returns what that means.
An error withdraws the claim: the server refused, so nothing is coming. A Held operation ends here. An Observed one is acknowledged, and from now on reads taken after this moment are entitled to end it.
Only the claims op itself made are touched, so a key re-dispatched since is left to its newer operation.
func (*Set) Handle ¶
Handle advances the animation. Components call it from Update and return the command; every message that is not a spinner tick is a no-op beyond the chance to notice the chain has stalled.
func (*Set) ObserveRows ¶ added in v0.30.0
func (s *Set) ObserveRows(keys []string, eval func(i int) (label string, busy bool, rev string)) tea.Cmd
ObserveRows is one observation of the rows a component holds: it runs eval over each and replaces the whole collection with what it reports.
keys are the rows' keys in order; eval(i) reports row i's label, whether it is busy, and an optional revision. The label and revision together are the row's status — what a dispatched claim compares against to notice the server acted — and the label alone is what a busy row shows. A nil eval means the component has no predicate: nothing is observed busy, but the read still counts down Observed claims, or a screen that dispatches without one would spin forever.
Wholesale rather than incremental because an observation *is* the whole truth as of that moment. A key's Since survives across observations, so a row that stays busy keeps measuring from when the work was first seen.
The returned command is the animation's first tick; a component batches it into its own command stream.
func (Set) ReadsFailing ¶ added in v0.30.0
ReadsFailing reports whether the newest read failed and no observation has arrived since. Rows busy only on the strength of an earlier read are drawn Unknown meanwhile; a screen typically says so on its title as well.
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 is not busy, so a caller can fall through to whatever the row normally shows — which is the whole of how an indicator ends. There is no outcome glyph, because when "running" becomes "failed" the cell goes back to rendering the row's own value, which already says "failed" in the app's own colours; a ✗ held over the top restates it less precisely.
type State ¶
type State struct {
// Label is what the row says — "running", "Syncing". It is the value the
// predicate matched, so it is the server's own word rather than one
// invented here.
Label string
// Since is when this key was first observed busy, not when it was last
// observed. Elapsed time therefore measures the work rather than the poll
// that last saw it.
Since time.Time
// Unknown is true while reads are failing and this key is busy only
// because the last successful read said so. The work may have finished
// since; nobody can see. Render draws it static, with a "?" where the
// spinner would be — neither cleared, which would invent a read nobody
// took, nor animated, which would claim someone is still watching.
Unknown bool
}
State is one key's in-flight state.
type UnobservedMsg ¶ added in v0.30.0
UnobservedMsg reports an Observed operation's keys that ended without any read ever reporting them busy.
Label is the claim's label as passed to Dispatch — usually the server's word for work in progress, "Running" — not the name of the verb, so m.Label+" finished" reads "Running finished". Name the verb yourself, from Op if the screen has several.
The work finished between two reads, was a no-op, or never started — from the rows alone these are the same, which is why this is a message and not an indicator.
It arrives through the component's Update on the turn after the read that ended the claim — ApplyRead and SetKeyedRows are setters and return nothing — so a screen that forwards every message to its component (rule 6) and matches this in its own Update receives it. Changed separates the first from the other two when the status moved while nobody was looking. Without this the row simply returns to what it said before, and the user concludes the action was ignored.