app

package
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Sep 6, 2026 License: Apache-2.0 Imports: 20 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.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type InFlight

type InFlight struct {
	List   func(ctx context.Context) ([]flight.Summary, error)
	Resume func(ctx context.Context, id string) (engine.PromotionState, flight.DriveFunc, error)
}

InFlight is how the root lists what is promoting right now for the matrix's pane, and re-drives one of them on the flight screen (M10: the TUI's `hoist promotions` and `hoist resume`). List re-observes every state file against the forge and the cluster — AGENTS.md §4.1, never the recorded phase — so it is called off the Update stack, at boot and then every Poll.Approval while the matrix is the top screen. Resume builds the same state and DriveFunc `hoist resume <id>` would. Both nil means the feature is not wired (a flags-only run, a test): the pane stays absent and r says so.

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, resolveFn plan.ResolveFunc, 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. resolveFn is what the plan screen calls to resolve digests; nil runs it in "digest sources: none" mode throughout. promo is what confirming a plan and driving the flight screen need — 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, same as a resolveFn failure does for plan). 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) Init

func (m Model) Init() tea.Cmd

Init asks the terminal for its background colour so the palette can follow it, and starts whatever the top (only, at boot) screen's own Init needs.

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 transient notice (see Model.notice) appended below it when one is set.

func (Model) WithDrift

func (m Model) WithDrift(resolveFn plan.ResolveFunc) Model

WithDrift hands the matrix a resolver of its own for asking the cluster what each env runs — cmd/hoist builds a pods-only one, since a registry fallback answers a question the drift column never asked.

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

func (m Model) WithInFlight(f InFlight) Model

WithInFlight supplies the in-flight listing and resume functions (cmd/hoist's buildInFlightFuncs). See InFlight.

type Promotion

type Promotion struct {
	Start      StartPromotionFunc
	Poll       flight.PollDurations
	OpenURL    func(url string) error
	OpenPRMode string
}

Promotion groups everything New needs to actually drive a confirmed plan and act on the flight screen's own requests, beyond what ResolveFunc already covers — the wiring PR #39 left as a stub (see plan.StartMsg's and flight.OpenPRMsg's cases below). Start is nil in a context with nothing to drive (mirrors ResolveFunc's own nil convention): the plan screen's Enter key then shows a notice instead of pushing a read-only flight screen. OpenURL is nil the same way: 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.

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 (round 5, finding 3: 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 StartOpts

type StartOpts struct {
	Direct    bool
	Confirmed bool
}

StartOpts is how a screen says which shape of promotion it confirmed. It is a struct rather than a bool so that adding a future mode does not change every call site's meaning silently.

Direct selects engine.AllDirectSteps over engine.AllSteps: commit straight to the base branch with no PR. Confirmed must be true only in direct response to the operator's own keypress-then-confirm gesture — engine.DirectCommitGateStep trusts it as the record of that gesture and refuses production regardless of it (internal/engine/direct.go), so a screen that sets it without one is not bypassing the gate, only lying to it.

type StartPromotionFunc

type StartPromotionFunc func(ctx context.Context, p gitops.Plan, opts StartOpts) (engine.PromotionState, flight.DriveFunc, error)

StartPromotionFunc builds a real engine.PromotionState and flight.DriveFunc for a plan the operator just confirmed (plan.StartMsg) — the id/branch/worktree derivation, the claim-then-rescan one-in-flight check, and the prior-state merge-in that cmd/hoist/promote.go's buildPromotionForConfirm already does for the CLI path (AGENTS.md §4.8's "cmd/hoist owns the adapter" rule: this package only ever sees the plain function type, never pkg/git, pkg/forge or internal/config themselves). It is called from inside a tea.Cmd (see the plan.StartMsg case below), never directly from Update, since it can talk to a real git remote and forge (AGENTS.md §4.3) — exactly like plan.ResolveFunc.

A non-nil error means the plan cannot start right now (a real in-flight conflict, missing github config, a claim failure, or every ticked edit already being a no-op — see cmd/hoist/wiring.go's own anyRealEdit guard) and is shown as a notice on whichever screen popped up plan.StartMsg, rather than pushing the flight screen at all. p is expected to already be filtered to the operator's ticked selection (see filterTicked below) — this type itself carries no notion of "ticked", only whatever Plan the caller hands it.

Directories

Path Synopsis
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 R key: the screen that shows what a restart would roll, takes the confirmation, and follows the rollout.
Package restart is the matrix's R key: the screen that shows what a restart would roll, takes the confirmation, and follows the rollout.
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).

Jump to

Keyboard shortcuts

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