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

app

package
v0.2.0 Latest Latest
Warning

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

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

Documentation

Overview

Package app is the root Bubble Tea model for the hoist TUI.

Shape (the convention in AGENTS.md §4.8, first adopted here):

  • internal/app holds the root tea.Model: the screen stack, the window size, the theme (built once from tea.BackgroundColorMsg) and the global keys (q / ctrl+c quit). It is the only tea.Model in the program; everything else is a screen.
  • internal/app/<screen> holds one screen as its own model with a New(...) constructor, a pure Update that returns the screen's concrete type, a View() string, and SetSize/SetStyles setters the root calls on resize and theme change. The root wraps each screen in a tiny adapter (see screen.go) so screens never import this package.
  • A screen's derived data — what it shows, before any styling — lives in a separate file with no terminal dependency (matrix/cells.go) so it is unit-testable as plain values; the model file only lays that data out.
  • internal/ui holds the shared Styles palette, the frame chrome (ui.Frame, ui.Box, ui.Columns, ui.Dialog), relative time and the status-bar helper; it imports Lip Gloss and x/ansi (width and strip), no Bubbles.
  • No layout library (AGENTS.md §4.7) means no flexbox-for-terminals dependency: a screen's View is ui.Frame{...}.Render(styles, w, h), built on lipgloss's own borders and joins, with the footer always the last line. Hand-assembled box characters are the thing that rule forbids (§4.8, the M10 amendment).
  • Every screen's tests render through internal/ui/uitest: goldens at 80×24 and 120×40, and keypresses driven through uitest.Keys rather than fields set by hand.

The stack gained pop with the first screen that opens on top of the matrix (internal/app/plan): a screen never calls back into app to push or pop itself — that would mean every screen importing app, which is exactly the cycle this package's shape exists to avoid. Instead a screen emits a message of its own concrete type (matrix's OpenPlanMsg to push the plan screen, plan's BackMsg to pop it) and the root recognizes those types in its own Update switch, since app is the one package that already imports every screen. New navigation should follow the same shape rather than growing a second one: define the message where the emitting screen lives, handle it in app.go.

A background drive belongs to the session, not to the screen watching it

internal/app/session.Controller — a value type, held as one field on the root Model — is the one place a promotion or deploy is actually started, resumed, stepped, abandoned or listed. Every background command it issues (a Start/Resume round trip, one Driver.Step poll, a listing) reaches the root as a session.Event, and the root's own Update has exactly one case for it (kept out of internal/parity's navigation registry on purpose — session.Event is deliberately not named *Msg, since it plumbs nowhere the operator can trigger directly; see internal/app/session's own doc comment). The flight screen that shows a drive's progress owns none of it: it is built already attached (flight.NewAttached) to one Controller entry, and every later change reaches that same instance through Mirror, driven by the root's own apply(app.go) routing session.Change values by BuildID. A screen requests a step (flight.ReobserveMsg, OverrideCINoneMsg, AbandonMsg) rather than taking one, so nothing about driving a promotion is owned by whichever screen instance happens to be showing it. Esc leaves the drive running and only stops mirroring it onto a screen; shift+x (AbandonMsg; bare x is retired) cancels one drive, and q's confirmed quit cancels every drive at once (session.Controller.StopAll). A drive outlives its screen and re-attaches on demand, since the Controller, not the screen, owns the ctx and the goroutine.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Model

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

Model is the root tea.Model: a stack of screens, the window size, and the theme, plus what a screen needs to open the plan screen (internal/app/plan) or the tag picker (internal/app/tags) without app.New having to be called again — the repo, the promotable prefixes, the envs config (pairs, production), the digest-resolution adaptor (nil in "digest sources: none" mode) and the tag-picker's own registry/forge adaptor (nil runs the picker with no data source at all, reported as its own error state rather than a panic).

func New

func New(repo *gitops.Repo, promotable []string, envs config.EnvsConfig, planFn plan.Func, svc Service, promo Promotion, tagsFn tags.BuildFunc, restartFn apprestart.Funcs) Model

New returns the root model with the matrix screen on the stack. promotable lists the image repo prefixes that count as first-party (the same list hoist plan --promotable takes). envs is the selected repo's envs config (production, pairs), zero-valued when there is none. planFn is svc.Plan (internal/service): what the plan screen calls to build a promotion's gitops.Plan, and what openDeploy below calls directly to build a deploy's. svc is the root's one seam for starting, listing, resuming and abandoning a promotion, and for refreshing the repo — see Service's own doc comment (internal/app/service.go); nil degrades every gesture it would serve to a notice, exactly as the old per-func nil checks did (now via session.Controller's own ErrNoBackend, session.New(svc, ...) below — Service's method set is a strict superset of session.Backend's, so a nil svc converts to a nil Backend too). promo carries what's left for driving the flight screen (poll cadence, the browser opener) — see Promotion's own doc comment. tagsFn is what the tag-picker screen calls to list and fetch registry/forge data for one image repo; nil opens the picker with no data source (it reports the resulting error itself). The theme starts dark and is replaced when the terminal reports its background.

func (Model) History

func (m Model) History() history.Funcs

History returns what WithHistory set — for the screen constructors in later M10 PRs and for cmd/hoist's own wiring test.

func (Model) InFlightIDs added in v0.2.0

func (m Model) InFlightIDs() []string

InFlightIDs lists every promotion id this session still tracks, sorted — cmd/hoist's own main.go prints these with their `hoist resume <id>` line once the program has actually quit (keymap rule 4: "on the way out it prints the ids still in flight"), so an operator who quit past a running drive (q's own confirm, or ctrl+c's immediate exit) is told exactly how to pick each one back up rather than having to remember or reconstruct it.

func (Model) Init

func (m Model) Init() tea.Cmd

Init asks the terminal for its background colour so the palette can follow it, starts whatever the top (only, at boot) screen's own Init needs, and starts the controller's own listing loop (session.Controller.Init: an immediate listing plus its recurring tick chain).

func (Model) Update

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

Update handles window size, theme and the global keys, and forwards everything else to the top screen.

func (Model) View

func (m Model) View() tea.View

View renders the top screen in the alternate screen buffer, with the root's own bottom row (bottomText/bottomStyle, above — the transient q hint, or else the activity row) on the terminal's last row when there is one to show.

The row's own line is taken OUT of the screen above rather than appended after it. Every screen draws through ui.Frame.Render, which emits exactly `height` lines, so a row merely appended to that landed on row height+1 and the alternate screen buffer never showed it: pressing enter on the deploy confirm screen and having the promotion refused — an in-flight conflict, a missing repos[].github, a claim conflict — was indistinguishable from a dead key, and the only way to read the reason was to re-run the equivalent command on the CLI (#164). Re-sizing the top screen here, on every render, rather than once when the entry was added, keeps this a pure render concern: nothing in the stack is mutated, and unlike the old notice this row is NOT cleared on the next keypress (Model.activity's own doc comment, TestNoticeSurvivesKeypress) — it only goes away once the log itself is empty, which never happens once the first entry lands.

func (Model) WithConfigView added in v0.2.0

func (m Model) WithConfigView(path string, found bool, text string) Model

WithConfigView supplies what C shows: the config file's path, whether it existed, and the effective config as text — cmd/hoist's cfg.Redacted().Marshal(), the same bytes `hoist config show` prints. Unset, C says there is nothing to show.

func (Model) WithDrift

func (m Model) WithDrift(drift matrix.DriftFunc) Model

WithDrift hands the matrix its cluster question — cmd/hoist's buildDriftFunc, the raw pod observations per env (every running build kept, #122), never the planning resolver, which picks one digest per repo and would collapse a partial rollout. nil leaves the matrix claiming nothing about the cluster.

func (Model) WithHistory

func (m Model) WithHistory(h history.Funcs) Model

WithHistory supplies the commit-history and migration-delta functions (cmd/hoist's buildHistoryFuncs). See Model.history.

func (Model) WithRefreshRepo added in v0.2.0

func (m Model) WithRefreshRepo(refresh matrix.RefreshRepoFunc) Model

WithRefreshRepo hands the matrix cmd/hoist's own re-read-origin function (#PR7) — F5 asks the cluster (WithDrift's own DriftFunc) and re-reads the repo alike.

func (Model) WithRun added in v0.2.0

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

WithRun tells the matrix which base branch and kube context this session runs against (the launch's --base/--kube-context, #105), so its title can say so.

func (Model) WithWatch added in v0.2.0

func (m Model) WithWatch(b watch.BuildFunc) Model

WithWatch supplies the watch screen's builder — cmd/hoist's buildWatchFunc, the read-only Get/Deployment/JobLike adapter `hoist watch` also uses. See Model.watchFn.

type Promotion

type Promotion struct {
	Poll       flight.PollDurations
	OpenURL    func(url string) error
	OpenPRMode string
	// Now and After are session.Controller's own clock/scheduler seam (session.Config), threaded
	// through here rather than reached for directly (AGENTS.md §4.8: this package builds
	// session.Config, but only cmd/hoist decides what a real run's clock is) so a test can drive
	// a real Start -> Step(landed) -> refresh sequence deterministically — a fixed Now and an
	// After that fires on request rather than actually sleeping — the same seam session.Config
	// already exposes to its own package's tests, now reachable from app.New. Both nil (every
	// real caller, and every test that never needs to drive a tick chain) keeps session.Config's
	// own defaults: time.Now and tea.Tick.
	Now   func() time.Time
	After func(d time.Duration, f func(time.Time) tea.Msg) tea.Cmd
}

Promotion groups everything New needs to actually drive a confirmed plan and act on the flight screen's own requests, beyond what plan.Func already covers. Poll/OpenURL/OpenPRMode are unrelated to the Service seam below and stay here: OpenURL is nil when no browser opener is wired in (flight.OpenPRMsg then falls back to the pre-wiring "not wired yet" notice rather than panicking on a nil call). OpenPRMode is one of "launch", "display" or "both" (cmd/hoist owns reading config.PreferencesConfig.OpenPR and resolving it to this plain string, per AGENTS.md §4.8 — this package only ever compares against string literals, never importing internal/config's own constants for it, matching Poll's own already-translated-from-config shape); empty behaves like "launch", so a caller that never sets it (a test, in particular) gets today's original launch-only behavior rather than a silently different one.

Starting and driving a promotion itself goes through session.Controller (New's own m.sess, built from svc and Poll), not through this struct.

type Screen

type Screen interface {
	Init() tea.Cmd
	Update(tea.Msg) (Screen, tea.Cmd)
	View() string
	SetSize(width, height int) Screen
	SetStyles(ui.Styles) Screen
	// CapturesText reports whether the screen is currently in a mode where an ordinary
	// letter key like "q" is text the operator is typing — a filter query, a huh field's own
	// "/" filter — rather than a command. The root's global quit binding (app.go's Update)
	// checks this before treating "q" as quit, and only forwards the key to the screen as
	// usual when it's true; ctrl+c is unaffected and always quits (the global binding used to
	// run unconditionally, before any screen's own key handling ever saw the press, so typing
	// "q" into the tag picker's filter box quit the whole program instead of typing). A screen
	// with no such mode returns false unconditionally.
	CapturesText() bool
}

Screen is what the root drives. Screens are values: every method returns the updated screen rather than mutating, so the root model stays a pure tea.Model.

type Service added in v0.2.0

type Service interface {
	Plan(ctx context.Context, req service.PlanRequest) (service.PlannedChange, error)
	StartPromotion(ctx context.Context, req service.StartRequest, h service.Hooks) (service.Drive, error)
	List(ctx context.Context, o service.ListOpts) ([]service.Listed, error)
	Resume(ctx context.Context, id string, o service.ResumeOpts) (service.Drive, error)
	Abandon(ctx context.Context, id string) ([]string, error)
	RefreshRepo(ctx context.Context) (service.RepoView, error)
	Repo() service.RepoView
}

Service is the root's one seam onto internal/service — narrow, consumer-side, and named for what the TUI actually calls, not for internal/service.Service's own full surface (AGENTS.md §4.8): the root depends on this interface, and cmd/hoist's *service.Service satisfies it directly). A test fakes it with a plain struct rather than a real Service backed by git/forge/Argo — the same reason flight.Driver and plan.Func are kept small and local instead of importing internal/service's whole client-cache type.

Every method here already exists on *service.Service with this exact signature (see internal/service/plan.go, start.go, promotions.go, repo.go) — this interface adds nothing, it only narrows what the root is allowed to assume it can call.

Directories

Path Synopsis
Package activity is the root's own record of what has happened this session — every promotion started, landed, blocked or failed, every abandon, every browser-launch outcome — kept as an append-only, capped Log (AGENTS.md §4.8) rather than the single transient "notice" string app.Model carried before this package existed.
Package activity is the root's own record of what has happened this session — every promotion started, landed, blocked or failed, every abandon, every browser-launch outcome — kept as an append-only, capped Log (AGENTS.md §4.8) rather than the single transient "notice" string app.Model carried before this package existed.
Package config is the read-only config screen (#104): what `hoist config show` and `hoist config path` print, on the TUI.
Package config is the read-only config screen (#104): what `hoist config show` and `hoist config path` print, on the TUI.
Package deploy is the confirm screen for writing one named image into one env — the "image bump" half of hoist's problem statement, reached with d on the matrix and a tag chosen in internal/app/tags.
Package deploy is the confirm screen for writing one named image into one env — the "image bump" half of hoist's problem statement, reached with d on the matrix and a tag chosen in internal/app/tags.
Package flight is the flight screen: shown once a promotion starts driving through engine.AllSteps (branch, commit, push, PR, CI green, approved, merged, then (M5) Argo refresh, Argo sync, rollout), it lists every step with a glyph for its current state, the active step's own human detail text, a stopwatch since the promotion started, and a togglable scrollback of PromotionState.History.
Package flight is the flight screen: shown once a promotion starts driving through engine.AllSteps (branch, commit, push, PR, CI green, approved, merged, then (M5) Argo refresh, Argo sync, rollout), it lists every step with a glyph for its current state, the active step's own human detail text, a stopwatch since the promotion started, and a togglable scrollback of PromotionState.History.
Package history holds the function types through which screens ask about commit history and migration deltas — types only, no Bubble Tea, no adaptor construction.
Package history holds the function types through which screens ask about commit history and migration deltas — types only, no Bubble Tea, no adaptor construction.
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.
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.
Package plan is the plan/confirm screen: it runs discovery + digest resolution + gitops.BuildPlan for one source/target env pair, shows a tickable list of image repos on the left and, on the right, what the promotion ships for the repo under the cursor — the commits between what the target declares and what the source resolved to, and the migrations among them (M10, #85 screen 04/12) — with the unified diff one key away.
Package plan is the plan/confirm screen: it runs discovery + digest resolution + gitops.BuildPlan for one source/target env pair, shows a tickable list of image repos on the left and, on the right, what the promotion ships for the repo under the cursor — the commits between what the target declares and what the source resolved to, and the migrations among them (M10, #85 screen 04/12) — with the unified diff one key away.
Package restart is the matrix's shift+r key: the screen that shows what a restart would roll, takes the confirmation, and follows the rollout.
Package restart is the matrix's shift+r key: the screen that shows what a restart would roll, takes the confirmation, and follows the rollout.
Package scope stamps an async command's result with the screen instance that issued it, so Update can tell a request its own current instance is still waiting on from one an earlier instance of the same kind of screen never got to see land.
Package scope stamps an async command's result with the screen instance that issued it, so Update can tell a request its own current instance is still waiting on from one an earlier instance of the same kind of screen never got to see land.
Package session is the controller that owns every drive a promotion or deploy goes through once the operator confirms it: starting it (internal/service.Service.StartPromotion), resuming one already on disk (Resume), stepping it to completion one poll at a time, abandoning it, and keeping the matrix's in-flight pane listed — all of it re-observed, never trusted from a screen's own memory (AGENTS.md §4.1).
Package session is the controller that owns every drive a promotion or deploy goes through once the operator confirms it: starting it (internal/service.Service.StartPromotion), resuming one already on disk (Resume), stepping it to completion one poll at a time, abandoning it, and keeping the matrix's in-flight pane listed — all of it re-observed, never trusted from a screen's own memory (AGENTS.md §4.1).
Package tags is the tag-picker screen: given one image repo, it lists the registry's own tags and, once each row's metadata loads, its created time and digest — sorted per AGENTS.md's M6 brief invariant 3 (prefer the app repo's own git tags for ordering when the image repo is mapped; fall back to the registry's own Created metadata otherwise).
Package tags is the tag-picker screen: given one image repo, it lists the registry's own tags and, once each row's metadata loads, its created time and digest — sorted per AGENTS.md's M6 brief invariant 3 (prefer the app repo's own git tags for ordering when the image repo is mapped; fall back to the registry's own Created metadata otherwise).
Package watch is the matrix's w key: one Argo Application's sync/health/revision and the rollout state of every workload its family declares, re-read on a cadence — the TUI face of `hoist watch --app <name>` (#101).
Package watch is the matrix's w key: one Argo Application's sync/health/revision and the rollout state of every workload its family declares, re-read on a cadence — the TUI face of `hoist watch --app <name>` (#101).

Jump to

Keyboard shortcuts

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