Help us shape the future of Go! Take the 2026 Go Developer Survey

matrix

package
v0.3.0 Latest Latest
Warning

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

Go to latest
Published: Oct 26, 2026 License: Apache-2.0 Imports: 22 Imported by: 0

Documentation

Overview

Package matrix is the env × family screen: one row per family, one column per env, the family's image tag in each cell with its state spelled out — pinned, unpinned, split, external, drifted — as a word an operator can read without a legend. cells.go derives the cell values from a discovered repo (and, when the cluster has answered, from what each env is actually running) with no terminal dependency; model.go lays them out.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Detail added in v0.2.0

func Detail(t Table, fam, env string, envIsProduction bool, inflight []flight.Summary) []string

Detail is the cell's own story: family, env, what the manifest says, what state word that earned, the drift sentence when applicable, whether the env is production, and — matched on Target only, the same coarse correlation MenuFor uses, since flight.Summary carries no family — any in-flight promotion into this env.

func FirstPartyRepos

func FirstPartyRepos(fam *gitops.Family, promotable []string) []string

FirstPartyRepos lists, sorted, the distinct first-party image repos a family runs in one env — what d offers to deploy: one opens the picker directly, several open a chooser first.

func Grid added in v0.2.0

func Grid(st ui.Styles, t Table, g GridState) []string

Grid renders the table's own bordered block: a header-divider rule, the header row (one cell per Table.Envs entry, already carrying whatever cursor/production marker the caller baked into the display copy of t — Grid itself knows nothing about config or which column is "selected" beyond g.Col, which only ever drives the CELL highlight), a header/body divider, then g.Height data rows starting at g.Offset (blank-padded if the table has fewer rows than that). It returns the block as a slice of lines rather than a joined string so a caller composing it into a larger section (the matrix's own View) can measure it with lipgloss.Height the same way every other section is measured.

Types

type Cell

type Cell struct {
	// Present is false when the family has no Application in this env; the other fields
	// are then zero and String renders blank.
	Present bool
	// Text is the tag shown: the single first-party tag, "v1/v2" when one build is pinned
	// under several tags (sorted, so the text does not depend on file order), "N images"
	// when the family's first-party occurrences span N image repos, or "N versions" when the
	// one image repo runs under N builds inside the env (see builds). A third-party-only
	// family shows the same for its third-party images.
	Text string
	// State is the word beside Text; "" for a present cell with no images at all. The split
	// word is judged per image repo, so a "N images" cell is split whenever any one of its
	// repos runs under two builds, and a "N versions" cell is always split: Text counts repos
	// or builds, State says whether any repo's builds disagree, and both come from builds.
	State State
	// Pinned: every first-party occurrence carries a digest.
	Pinned bool
	// Differs: the set of refs differs from the previous env column's. Never set on the
	// first column or when the previous column is blank. Kept as data for a caller that
	// wants to colour a change across columns; it is no longer a glyph.
	Differs bool
	// ThirdParty: the family has no first-party image at all.
	ThirdParty bool
	// Running is what the cluster reports for a drifted cell, for the sentence under the
	// table ("marketing runs sha-77c0ffe here; manifest says …"): the one running build's
	// tag or short digest, or "N builds running (a, b)" when the pods run several. Builds
	// are counted by digest where the pod reported one (else by tag), and two builds under
	// the same tag are each named "tag · <short digest>" so they can be told apart — the
	// case a mutable tag makes most likely. Empty unless State is StateDrifted.
	Running string
	// Compared names the comparison that decided the drift — "by digest" when any manifest
	// reference for the repo is pinned and was compared with the undeclared build's digest,
	// "by tag" only when every comparison made for that build was against a bare manifest
	// reference — so the sentence says the strongest evidence it has. Empty unless State is
	// StateDrifted.
	Compared string
	// contains filtered or unexported fields
}

Cell is one family × env intersection.

func (Cell) String

func (c Cell) String() string

String is the cell as the table shows it: text, two spaces, state — or blank when absent. The table aligns the state to the right of the column itself (see rows in model.go); this is the unaligned form for tests and logs.

type DriftFunc

type DriftFunc func(ctx context.Context, env string) (map[string][]image.Ref, error)

DriftFunc reports what one env's cluster is running — the raw pod observations, keyed by canonical image repo, every distinct running reference kept (see Running) — asked for per env at boot so the matrix can say "drifted" where the manifest and the cluster disagree. It is not the planning resolver, which picks one digest per repo and would collapse a partial rollout (#122). nil means no cluster is configured and no cell is ever claimed drifted (nor not drifted). cmd/hoist builds it over pkg/k8s (AGENTS.md §4.8: this package takes a function value, never an adaptor).

type DriftMsg

type DriftMsg struct {
	// contains filtered or unexported fields
}

DriftMsg is one env's answer. from is the scope ID of the refresh it belongs to (scope.New at construction and again on each refresh), so a slow answer from before a refresh (F5) or a previous instance of this screen is dropped rather than painting stale drift over a newer answer.

func (DriftMsg) StampedBy added in v0.3.0

func (d DriftMsg) StampedBy() scope.ID

StampedBy implements scope.Stamped.

type Focus added in v0.2.0

type Focus uint8

Focus says which part of the matrix screen currently owns the cursor keys (tab moves it, the screen's own new binding): the grid itself, or the in-flight pane underneath it.

const (
	// FocusGrid is the default: up/down move the family row, left/right the env column.
	FocusGrid Focus = iota
	// FocusPane means up/down move the in-flight pane's own cursor instead, and enter/shift+x
	// act on the promotion under IT rather than opening the action menu for a cell.
	FocusPane
)

type GridState added in v0.2.0

type GridState struct {
	Row, Col, Offset, Height int
	Widths                   []int
	Focus                    Focus
}

GridState is the grid renderer's own input beyond the Table text itself (the audit doc's shape): the cell cursor (Row, Col — Col is an index into Widths' env columns, i.e. Table.Envs, not into Widths itself, which carries one extra leading entry for FAMILY), the vertical scroll Offset and how many data rows fit (Height), each column's fitted width (Widths[0] is FAMILY; Widths[i+1] is Table.Envs[i]), and which side of the screen currently has the cursor (Focus) — the grid dims nothing based on Focus itself; a screen that wants to show the pane focused draws that separately. Cursor cell styling instead only applies to the exact (Row, Col) intersection when Focus is FocusGrid, so tabbing to the pane visibly moves the highlight off the table rather than leaving a stale cell lit.

type MenuItem struct {
	B       keys.Binding
	Label   string
	Msg     tea.Msg
	Enabled bool
}

MenuItem is one row of the action menu: a Binding when the item has its own letter (so a keypress can run it directly without moving the cursor first), the label shown beside it, the message running it emits, and whether the cursor's own cell actually supports it right now (a disabled item is still listed — v2·02 shows disabled writes on a production column with an "(asks)"-style constraint stated once, never a hidden action the operator wonders where it went, AGENTS.md principle 5's own "warn, don't block").

func MenuFor(t Table, fam, env string, envs config.EnvsConfig, inflight []flight.Summary) []MenuItem

MenuFor lists what enter can do for the cell (fam, env): promote into env, deploy a tag, watch, restart, plus one row per in-flight promotion already targeting env — matched on Target alone, since flight.Summary carries no family (a promotion targets an env, not one row of it), which is this function's own known imprecision when two families are promoting into the same env at once (ticketed rather than chased here).

type Model

type Model struct {
	// contains filtered or unexported fields
}

Model is the matrix screen. It is a value: Update, SetSize and SetStyles return the updated model.

func New

func New(repo *gitops.Repo, promotable []string, envs config.EnvsConfig, drift DriftFunc) Model

New builds the screen for a discovered repo. promotable lists the first-party image repo prefixes (see Compute). envs is the repo's policy. drift is how the cluster is asked what each env runs; nil never asks. Columns come in pipeline order (envs.pairs) rather than alphabetically, and the cell cursor starts on the first non-production column so a fresh session never opens with the cursor already pointed at a write that asks for approval. The model has no size until SetSize is called.

func (Model) CapturesText

func (m Model) CapturesText() bool

CapturesText reports whether some dialog is open that takes letters as text — the image chooser's "/" filter, or its own confirm dialogs — and the root's q-to-quit must not fire over it. The action menu counts: it runs an item on a letter, so the root's q, l and ? must reach the menu (which ignores them) rather than quit, push the log or open help over it (UX-H13).

func (Model) CurrentEnv

func (m Model) CurrentEnv() string

CurrentEnv is the env the column cursor is on, "" when the repo has none.

func (Model) CurrentFamily

func (m Model) CurrentFamily() string

CurrentFamily is the family the row cursor is on, "" when the matrix has no rows.

func (Model) Cursor

func (m Model) Cursor() int

Cursor is the index of the selected family row.

func (Model) InFlight

func (m Model) InFlight() []flight.Summary

InFlight is what the pane currently shows.

func (Model) Init

func (m Model) Init() tea.Cmd

Init asks the cluster what every env is running, one command per env, when a DriftFunc was supplied.

func (Model) IsProduction

func (m Model) IsProduction(env string) bool

IsProduction reports whether env is listed in envs.production.

func (Model) KeyScreen added in v0.2.0

func (m Model) KeyScreen() keys.Screen

KeyScreen implements the root's keyed interface: the matrix now has a stated row in internal/ui/keys' registry, so the root's own "?" help overlay and "l" activity-log handling apply to it exactly as they already do to watch/restart/config/activity.

func (Model) Matrix

func (m Model) Matrix() Table

Matrix is the computed matrix the screen shows.

func (Model) Repo added in v0.2.0

func (m Model) Repo() *gitops.Repo

Repo returns the *gitops.Repo the table is currently computed from.

func (Model) RequestRefresh added in v0.2.0

func (m Model) RequestRefresh() (Model, tea.Cmd)

RequestRefresh is the completion-triggered refresh: exactly what F5 already does, exported so the root can call it when a drive lands or finishes.

func (Model) SetInFlight

func (m Model) SetInFlight(list []flight.Summary, err error) Model

SetInFlight replaces what the pane shows. err is the listing's own failure (the state directory unreadable), shown in the pane's place; a per-promotion re-observation failure travels inside its Summary.

func (Model) SetSize

func (m Model) SetSize(width, height int) Model

SetSize fits the grid to a width × height terminal.

func (Model) SetStyles

func (m Model) SetStyles(s ui.Styles) Model

SetStyles applies a palette.

func (Model) TakeRefreshError added in v0.2.0

func (m Model) TakeRefreshError() (string, Model)

TakeRefreshError returns and clears a failed F5's own reason, so the root can log it to the activity log instead of leaving it as the matrix's own clear-on-next-key notice — an async refresh's failure can easily land after the operator has already pressed another key.

func (Model) Update

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

Update handles the screen's keys.

func (Model) View

func (m Model) View() string

View renders the frame: the subheader, the grid, the notes, the in-flight pane (inside the frame) and the footer, with the menu or a chooser dialog over it when one is open.

func (Model) WithDrift

func (m Model) WithDrift(drift DriftFunc) Model

WithDrift replaces the cluster-asking function.

func (Model) WithNotice

func (m Model) WithNotice(notice string) Model

WithNotice sets the notice shown under the table.

func (Model) WithNow

func (m Model) WithNow(now func() time.Time) Model

WithNow fixes the clock the pane ages promotions with (tests); the default is time.Now.

func (Model) WithRefreshRepo added in v0.2.0

func (m Model) WithRefreshRepo(refresh RefreshRepoFunc) Model

WithRefreshRepo installs the function F5 calls to re-read the repo.

func (Model) WithRepo added in v0.2.0

func (m Model) WithRepo(repo *gitops.Repo) Model

WithRepo replaces the *gitops.Repo the table is computed from.

func (Model) WithRun added in v0.2.0

func (m Model) WithRun(base, kubeContext string) Model

WithRun names the base branch and kube context this session runs against (#105).

type OpenConfigMsg added in v0.2.0

type OpenConfigMsg struct{}

OpenConfigMsg is emitted when the operator asks to read the effective config (c, #104): the TUI's `hoist config show`. It carries nothing — the root holds the text and the path.

type OpenPlanMsg

type OpenPlanMsg struct {
	Source, Target string
}

OpenPlanMsg is emitted when the operator asks to plan a promotion into Target (p, or the action menu's own "promote into" item). Source is the one reverse pair (envs.pairs) found for Target when exactly one exists; empty when there is none or several, in which case the plan screen itself asks "promote into Target from…" (promote now always goes by the TARGET the cursor is on, never a forced prompt for an arbitrary target — the retired P key's own job).

type OpenRestartMsg

type OpenRestartMsg struct {
	Family, Target string
}

OpenRestartMsg is emitted when the operator asks to restart the family under the cursor in CurrentEnv (shift+r).

type OpenTagsMsg

type OpenTagsMsg struct {
	ImageRepo, Target string
}

OpenTagsMsg is emitted when the operator asks to pick a new tag for the current cell (t, "deploy a tag" — the tag picker, internal/app/tags). ImageRepo is the one first-party image repo the family runs in CurrentEnv, or the one the operator chose when there were several.

type OpenWatchMsg added in v0.2.0

type OpenWatchMsg struct {
	Family, Target string
}

OpenWatchMsg is emitted when the operator asks to watch the family under the cursor converge in CurrentEnv (w).

type RefreshRepoFunc added in v0.2.0

type RefreshRepoFunc func(ctx context.Context) (*gitops.Repo, string, error)

RefreshRepoFunc re-reads the repo this matrix shows — cmd/hoist's own fetch-then-discover against a cached origin/<base> view (#PR7), never the operator's own working tree (AGENTS.md §4.6). Mirrors DriftFunc's own shape: a plain function type this screen holds and calls (§4.8 — a screen may call a function type it's handed, just never import the git/network machinery behind it itself), built once by cmd/hoist and installed with WithRefreshRepo. The returned rev is origin/<base>'s tip at the moment this view was built (svc.RefreshRepo's own RepoView.SHA, #PR8) — "" when the fetch fell back to the clone, since there is then nothing freshly read to name.

type RepoRefreshedMsg added in v0.2.0

type RepoRefreshedMsg struct {
	Gen  scope.ID
	Repo *gitops.Repo
	Rev  string
	Err  error
}

RepoRefreshedMsg carries RefreshRepoFunc's answer back to Update. Gen is the scope ID of the askRepoRefresh call that issued it, the same way DriftMsg's own from does for the cluster fan-out. A Model that has not yet asked holds the zero ID, which is what an unstamped Gen of 0 matches.

func (RepoRefreshedMsg) StampedBy added in v0.3.0

func (r RepoRefreshedMsg) StampedBy() scope.ID

StampedBy implements scope.Stamped.

type ResumeMsg

type ResumeMsg struct {
	ID    string
	Build session.BuildID
}

ResumeMsg asks the root to re-drive one in-flight promotion on the flight screen (r, or enter on the pane) — the TUI's `hoist resume <id>`. Build carries the session.Controller BuildID for an entry still Building: ID is empty until a real promotion id exists, so re-attaching to it before then has nothing else to key by — the root re-attaches by Build directly (session.Controller.BuildSnapshot) rather than calling Resume with an empty id, which would fail to find anything and, worse, would have started a second drive had Resume("") happened to succeed.

type Row

type Row struct {
	Family string
	Cells  []Cell
}

Row is one family across every env; Cells is aligned with Table.Envs.

type Running

type Running map[string]map[string][]image.Ref

Running is what each env's cluster is actually running, keyed env → image repo (in the registry's canonical spelling, image.Canonical) → every distinct running reference (the digest each pod reports, and the tag the pod was started from when it reported one). Every build is kept: a partial rollout is two entries, never the one a resolver would choose (#122). nil, or a missing env, means the cluster has not answered for that env — no cell there can be drifted, and none is claimed not to be.

type State

type State string

State is the one word a cell shows beside its tag. Colour reinforces it; it never replaces it (docs/tui/mockups.html: "state is a word, not a glyph").

const (
	// StatePinned means every first-party occurrence carries a digest.
	StatePinned State = "pinned"
	// StateUnpinned means at least one first-party occurrence is a bare tag — a moved tag is
	// invisible to imagePullPolicy: IfNotPresent (AGENTS.md principle 3).
	StateUnpinned State = "unpinned"
	// StateSplit means one image repo runs under more than one build inside the env (two
	// digests, or two tags — see builds) — the condition the runbook blocks a promotion on,
	// so it should look like a problem. Judged per repo, so a multi-repo cell can be split too.
	StateSplit State = "split"
	// StateExternal means the family has no first-party image at all.
	StateExternal State = "external"
	// StateDrifted means the cluster runs a build the manifest does not declare. The single most
	// important fact on the screen when it is true, so it gets a sentence under the table too.
	StateDrifted State = "drifted"
)

type Table

type Table struct {
	Envs []string
	Rows []Row
}

Table is the whole matrix: envs sorted by name, families sorted by name (the union across envs).

func Compute

func Compute(r *gitops.Repo, promotable []string, running Running) Table

Compute derives the matrix from a discovered repo. promotable lists the image repo prefixes that count as first-party (what hoist plan --promotable takes); an occurrence matching none is third-party. running may be nil.

func Order added in v0.2.0

func Order(t Table, envs config.EnvsConfig) Table

Order reorders t's columns into the pipeline order envs.PipelineOrder derives from envs.pairs (UX-M9's own header-order companion): a promotion flows source-to-target, so the matrix should read that way left-to-right instead of alphabetically. Pure: every Row's Cells is permuted along with Envs so index i in the result still means the same env for both, and no cell's own content is touched.

Jump to

Keyboard shortcuts

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