activity

package
v0.27.0 Latest Latest
Warning

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

Go to latest
Published: Sep 14, 2026 License: MIT Imports: 7 Imported by: 0

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

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

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

func Busy(values ...string) func(value string) (label string, busy bool)

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

func Progress(out io.Writer, text string)

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

func Settled(values ...string) func(value string) (label string, busy bool)

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 EndMsg

type EndMsg struct {
	RunID int64
	Err   error
}

EndMsg finishes every key belonging to RunID, with Err deciding the outcome glyph.

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 New

func New(opts Options) Set

New builds a Set from opts.

func (Set) Active

func (s Set) Active() bool

Active reports whether any key has an entry — running, holding an outcome, or derived from the data.

func (*Set) Adopt

func (s *Set) Adopt(other Set) tea.Cmd

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

func (s Set) Badge(key, row string, width int) string

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

func (s *Set) Clear(key string)

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) ClearAll

func (s *Set) ClearAll()

ClearAll retires everything.

func (Set) Count

func (s Set) Count() int

Count is how many keys are still running, held outcomes excluded.

func (*Set) Derive

func (s *Set) Derive(busy map[string]string) tea.Cmd

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) Finish

func (s *Set) Finish(key string, err error) tea.Cmd

Finish marks one key done and starts its hold. A nil err is a success.

func (*Set) FinishRun

func (s *Set) FinishRun(runID int64, err error) tea.Cmd

FinishRun marks every key of a run done.

func (*Set) Handle

func (s *Set) Handle(msg tea.Msg, holds func(key string) bool) tea.Cmd

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

func (s *Set) Relabel(key, label string)

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

func (s *Set) RelabelRun(runID int64, label string)

RelabelRun changes what every key of a run says.

func (Set) Render

func (s Set) Render(key string, width int) (string, bool)

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

func (s *Set) Revise(changes map[string]Change) tea.Cmd

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.

func (*Set) Start

func (s *Set) Start(key, label string) tea.Cmd

Start begins activity on one key, replacing any entry already there.

func (*Set) StartRun

func (s *Set) StartRun(keys []string, label string, runID int64) tea.Cmd

StartRun begins activity on every key of a run.

func (Set) State

func (s Set) State(key string) (State, bool)

State reports one key's state, local layer first.

func (Set) Width

func (s Set) Width(key string, width int) int

Width is the visible width Render would produce for key, before styling. Callers that lay out around the indicator need it without paying for the escapes.

type StartMsg

type StartMsg struct {
	Keys  []string
	Label string
	RunID int64
}

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.

func (State) Failed

func (s State) Failed() bool

Failed reports whether the work finished unsuccessfully.

func (State) Running

func (s State) Running() bool

Running reports whether the work is still in flight.

type UpdateMsg

type UpdateMsg struct {
	RunID int64
	Label string
}

UpdateMsg relabels every key belonging to RunID — the row-facing half of an action's progress reporting. See Progress.

Jump to

Keyboard shortcuts

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