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

tui

package
v0.29.2 Latest Latest
Warning

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

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

Documentation

Overview

Package tui implements Atelier's Bubble Tea TUI.

The TUI is a two-pane layout (left=variable list, right=editor) with a status pane at the bottom (SPEC §7, ADR-0006). The top-level Model owns the wrapper.State and routes input to the active editor based on the selected variable's type.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func BuildPlanTree

func BuildPlanTree(plan *tfjson.Plan) *planNode

BuildPlanTree groups a parsed plan's ResourceChanges into a (module → type → resource) tree. No-op changes are filtered out (consistent with `terraform plan` itself). Public for tests; the TUI doesn't import it from outside.

func BuildStateTree

func BuildStateTree(s *state.State) *planNode

BuildStateTree builds a plan-tree-compatible structure from the terraform state, grouped by module → type → resource. This allows navigating state resources using the same tree UI when the plan has no changes.

func PlanSummary

func PlanSummary(plan *tfjson.Plan) string

PlanSummary returns the standard one-line counts header.

Types

type Applier

type Applier interface {
	// ApplyCmd returns a command that applies the most recent saved plan file
	// with the terminal handed back to terraform, so its own output and
	// approval prompt reach the user unmediated (ADR-0052).
	ApplyCmd() tea.Cmd
}

Applier is the narrow interface the TUI needs to apply a saved plan. Separated from Planner so the capability can be independently stubbed or disabled.

type AttributeDiffLine

type AttributeDiffLine struct {
	Marker string // "+", "-", "~"
	Key    string
	Before string // empty for create
	After  string // empty for delete
}

AttributeDiffLine is one rendered diff line in the right-pane attribute diff view: a key, an action indicator, and one or two value renderings.

func AttributeDiff

func AttributeDiff(rc *tfjson.ResourceChange) []AttributeDiffLine

AttributeDiff renders the attribute-level diff for a single resource change. Sensitive attributes are masked as "<sensitive>".

Behaviour by action:

  • create: every After attribute as `+`
  • update: keys whose values differ → `~`; new keys → `+`; removed keys → `-`
  • delete: every Before attribute as `-`
  • replace (delete+create): show like update (forces a clear visual signal)
  • read / no-op: empty result

func (AttributeDiffLine) String

func (l AttributeDiffLine) String() string

String renders the line per the convention SPEC §7.4 implies.

type CheckWarning added in v0.1.0

type CheckWarning struct {
	// Address is the display address of the check block, e.g.
	// "check.grafana_storage_directives".
	Address string
	// Message is the error_message the check author wrote.
	Message string
}

CheckWarning is one failed `check` block assertion, distilled from the plan's Checks section into just the two things the UI needs: a display address (e.g. "check.grafana_storage_directives") and the author's error_message.

Only `check` blocks are surfaced here. Resource/output pre- and post-conditions (the other CheckKinds) fail the plan itself and are therefore already carried by the plan-error path; check blocks are unique in that they degrade to advisory warnings that never block plan or apply, so they need a dedicated, non-error surface.

func FailedChecks added in v0.1.0

func FailedChecks(plan *tfjson.Plan) []CheckWarning

FailedChecks extracts the failed `check` block assertions from a parsed plan. It reads the machine-readable Checks section that `terraform show -json` emits, so it never scrapes human-readable stdout and never drifts from Terraform's own semantics.

Only checks with Kind == "check" and Status == "fail" are returned; passing checks, resource/output conditions, and unknown/errored checks are skipped. The result is deterministic (input order preserved) and nil when the plan has no failed check blocks.

type Editor

type Editor interface {
	Update(msg tea.Msg) (Editor, tea.Cmd)
	View() string
}

Editor is the interface every type-specific right-pane editor satisfies.

type EditorWithCursor

type EditorWithCursor interface {
	Editor
	CursorLine() int
}

EditorWithCursor is an editor that reports its logical cursor line (0-based) so the right-pane scroll can follow the cursor.

type EditorWithValue

type EditorWithValue interface {
	Editor
	CurrentValue() cty.Value
}

EditorWithValue is an editor that can report its current value back to the top-level model (so edits flow into state.Values).

type Model

type Model struct {
	State *wrapper.State
	// Modules holds all module states for multi-module wrappers. When
	// len(Modules) > 1, the left pane shows section headers to group
	// variables by module. When empty or len==1, behaves as before.
	Modules []ModuleEntry

	// Module display info shown in the status bar.
	ModuleName   string
	LiteralRef   string
	ResolvedSHA  string
	SourceURL    string
	ManifestPath string

	// WrapperDir is the directory containing main.tf and terraform.tfstate.
	// Used for reloading state after apply.
	WrapperDir string

	// Planner runs `terraform plan` asynchronously when the user presses P.
	// May be nil (e.g. in tests or read-only contexts); the P key just
	// produces a friendly status message in that case.
	Planner Planner

	// Applier runs `terraform apply` when the user presses A from the plan
	// view. May be nil; the A key is hidden if unset.
	Applier Applier

	// Validator runs `terraform validate` after edits (debounced). May be
	// nil; validation is skipped if unset.
	Validator Validator

	// RefSwitcher handles the backend logic of switching module refs.
	// May be nil (e.g., local source wrappers where ref switching is N/A).
	RefSwitcher RefSwitcher

	// RefUnresolved, when non-nil, marks the primary module as opened with a
	// ref that no longer resolves on the remote (e.g. the branch was deleted).
	// The TUI auto-opens the ref-switch modal on startup and shows a banner
	// explaining why the variable schema is unavailable.
	RefUnresolved *RefUnresolvedInfo
	// contains filtered or unexported fields
}

Model is the top-level TUI model.

func New

func New(state *wrapper.State, modName string) *Model

New builds a Model around a wrapper.State.

func (*Model) ActiveModuleState

func (m *Model) ActiveModuleState() *wrapper.State

ActiveModuleState returns the wrapper.State for the variable currently under the cursor. Falls back to the primary State.

func (*Model) AddModule

func (m *Model) AddModule(state *wrapper.State, name string)

AddModule appends an additional module to the TUI. Variables from all modules are shown in the left pane, grouped under section headers when there is more than one module.

func (*Model) AddModuleEntry

func (m *Model) AddModuleEntry(e ModuleEntry)

AddModuleEntry appends a fully-populated module entry (including its ref identity and per-module switcher) to the session.

func (*Model) Init

func (m *Model) Init() tea.Cmd

Init implements tea.Model.

func (*Model) SaveIfDirty

func (m *Model) SaveIfDirty() error

SaveIfDirty writes the state to disk if there were pending edits. Returns the error from the write, or nil. Exposed so the runtime layer (cmd) can run a final flush before exit.

func (*Model) SelectedPlanChange

func (m *Model) SelectedPlanChange() *tfjson.ResourceChange

SelectedPlanChange returns the resource_change under the plan cursor, or nil when the cursor is on a non-leaf row (module/type) or no plan exists.

func (*Model) SelectedVariable

func (m *Model) SelectedVariable() *tfvars.Variable

SelectedVariable returns the variable currently under the cursor, or nil if out of range.

func (*Model) SetTFState

func (m *Model) SetTFState(s *state.State)

SetTFState sets the parsed terraform state for display in the plan view.

func (*Model) Update

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

Update implements tea.Model.

func (*Model) View

func (m *Model) View() string

View implements tea.Model.

type ModuleEntry

type ModuleEntry struct {
	State *wrapper.State
	Name  string // display name (typically the module block name)

	// Ref identity for THIS specific module. Populated for git-sourced
	// modules so each module can be ref-switched independently.
	SourceURL   string
	Ref         string
	ResolvedSHA string
	// Switcher performs the ref switch for this module. Nil for local
	// sources (no remote to switch); the R key is a no-op for those.
	Switcher RefSwitcher
}

ModuleEntry represents one module in a multi-module wrapper session.

type Planner

type Planner interface {
	// EnsureInit runs `terraform init` once if needed. Subsequent calls are
	// fast no-ops.
	EnsureInit(ctx context.Context) error
	// Plan runs `terraform plan -out=<tmp>; terraform show -json <tmp>` and
	// returns the parsed JSON plan.
	Plan(ctx context.Context) (*tfjson.Plan, error)
}

Planner is the narrow interface the TUI needs from a terraform executor. Defined here (rather than depending on tfexec.Terraform directly) so tests can substitute a stub.

type RefSwitchResult

type RefSwitchResult struct {
	State       *wrapper.State
	ResolvedSHA string
	LiteralRef  string
	// OrphanedVars lists variable names that had user-set values in the
	// previous state but no longer exist in the module at the new ref.
	OrphanedVars []string
	// NewVars lists variables added in the new ref that were not present
	// in the previous state.
	NewVars []tfvars.Variable
	// InitIncomplete is true when the post-switch `terraform init -upgrade`
	// did not finish cleanly. The switch still succeeds: the module is
	// fetched (module installation precedes config validation) and the new
	// schema is shown. The most common cause — the new ref added a required
	// variable that isn't filled yet — is detected and phrased by the TUI
	// from its own state, so this flag only needs to convey the bare signal.
	// The planner re-runs init -upgrade on the next plan, and
	// `terraform validate` surfaces the specific diagnostics in the meantime.
	InitIncomplete bool
}

RefSwitchResult is returned by SwitchRef with the new state, any orphaned variable names (present in old state but absent in new module), and the resolved SHA.

type RefSwitcher

type RefSwitcher interface {
	// SwitchRef re-clones the module at the new ref, re-parses variables,
	// runs terraform init -upgrade, and returns the updated state. The
	// caller is responsible for merging user overrides from the old state.
	SwitchRef(ctx context.Context, newRef string) (*RefSwitchResult, error)

	// ListRefs returns the human-friendly ref names (branches and tags) the
	// module's remote currently has, for presentation in the ref-switch
	// modal. It is the source of truth for the "available refs" hint so the
	// list is never stale — including after an in-session switch fails or a
	// ref is deleted mid-session. Returns an error when the remote can't be
	// reached; callers treat that as non-fatal (the input stays free-text).
	ListRefs(ctx context.Context) ([]string, error)
}

RefSwitcher is the narrow interface the TUI needs to switch the module ref. Defined as an interface so tests can substitute a stub.

type RefUnresolvedInfo

type RefUnresolvedInfo struct {
	Ref       string
	Reason    string
	Available []string
	Offline   bool
}

RefUnresolvedInfo mirrors bootstrap.RefUnresolved for the TUI layer: it records why the wrapper opened without a resolvable ref so the model can auto-open the switch modal, seed the hint, and render an explanatory banner.

type TfexecPlanner

type TfexecPlanner struct {
	Tf         *tfexec.Terraform
	WrapperDir string
	// contains filtered or unexported fields
}

TfexecPlanner is the production Planner: it shells out via terraform-exec against a wrapper directory.

func (*TfexecPlanner) ApplyCmd added in v0.23.1

func (p *TfexecPlanner) ApplyCmd() tea.Cmd

ApplyCmd applies the cached plan file with the terminal handed back to terraform: tea.ExecProcess releases the alt-screen so terraform prints its own progress. The plan was already reviewed in the plan tree and pressing A is the confirmation, so the flags match what the plan-file path has always passed rather than prompting a second time.

func (*TfexecPlanner) EnsureInit

func (p *TfexecPlanner) EnsureInit(ctx context.Context) error

EnsureInit runs `terraform init` if modules have not been installed in the wrapper. The check looks for .terraform/modules/modules.json which Terraform writes when module sources are fetched. This catches the case where .terraform/ exists (from provider init) but the module block's source was added or changed since the last init.

func (*TfexecPlanner) Plan

func (p *TfexecPlanner) Plan(ctx context.Context) (*tfjson.Plan, error)

Plan runs the plan and parses the resulting plan file. The temporary plan file lives under .atelier/cache/ so it doesn't pollute the wrapper root and is regenerable.

func (*TfexecPlanner) ResetInit

func (p *TfexecPlanner) ResetInit()

ResetInit clears the cached init state so the next EnsureInit call will run init -upgrade. Called after a ref switch rewrites the module source. An init already in flight is invalidated rather than cancelled: its result described the old module source, so it must not mark the wrapper ready.

func (*TfexecPlanner) Validate

func (p *TfexecPlanner) Validate(ctx context.Context) (*tfjson.ValidateOutput, error)

Validate runs `terraform validate -json` against the wrapper directory.

type Validator

type Validator interface {
	// Validate runs `terraform validate -json` and returns diagnostics.
	Validate(ctx context.Context) (*tfjson.ValidateOutput, error)
}

Validator is the narrow interface the TUI needs to run `terraform validate`. Separated from Planner so the capability can be independently stubbed.

Jump to

Keyboard shortcuts

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