tui

package
v1.30.1 Latest Latest
Warning

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

Go to latest
Published: Aug 8, 2026 License: MIT Imports: 21 Imported by: 0

Documentation

Index

Constants

View Source
const SearchPageSize = 10

SearchPageSize is DataProvider.Search's defensive fallback page size, used by every implementation whenever a caller passes pageSize <= 0 (a stray direct call, or a test that doesn't care about sizing), and the floor of m.searchFetchSize()'s derived clamp range (app.go) - a floor-height terminal's search still returns this many results rather than shrinking further. Originally mirrored the CLI picker's own fixed displayPageSize (cmd/lmm/install.go) as the TUI's ONLY page size; #111 Tier 1 replaced that fixed use with a per-session size derived from the visible pane (see searchFetchSize), leaving this constant with the floor/fallback role only.

Variables

This section is empty.

Functions

This section is empty.

Types

type ActionOutcome added in v1.11.0

type ActionOutcome struct {
	Message  string   // one-line success summary, e.g. `Enabled "SkyUI"` / "Deployed 5 mod(s)"
	Warnings []string // non-fatal diagnostics: the underlying flow result's Warnings then Notes, in that order

	// ImportedProfile names the profile a successful ApplyImport (Phase 6b
	// Task 9) just saved, but ONLY when it targets the session's CURRENTLY
	// ACTIVE game - the signal app.go's actionDoneMsg handler uses to
	// dispatch a deferred "switch to it now?" offer (mutations.go's
	// importAppliedMsg/resolveImportApplied). Every other ActionProvider
	// method, and a cross-game import (the imported profile's own declared
	// game differs from the active one), leaves this "" - treated
	// identically to switchedTo's own "nothing to do" zero value (see
	// actionDoneMsg's doc comment), except this NEVER rebinds anything by
	// itself: it only names a candidate the session MIGHT switch to next,
	// pending explicit user confirmation via the offer.
	ImportedProfile string

	// ResultLines is an OPTIONAL list of per-item detail lines for a batch
	// outcome (fix-wave-2 smoke finding #2): only applyUpdatesSequentially
	// (mutations.go), the apply-updates batch's confirm-time body, populates
	// this today - one "✓ <name> <from> → <to>" line per successful update,
	// one "✗ <name>: <error>" line per failed one, in the SAME order the
	// batch was applied, plus (#259) one trailing section - a blank
	// separator, then one line per distinct success-emitted warning - when
	// any successful update carried Warnings (see applyUpdatesSequentially's
	// doc comment for why they must ride here). Every other ActionProvider
	// call leaves this nil, same as ImportedProfile's own "" zero value
	// above - app.go's actionDoneMsg handler treats a nil/empty ResultLines
	// as "nothing to show" and opens no overlay for it. This is a TUI-side
	// struct, not part of the ActionProvider interface itself, so adding it
	// required no interface/method change on either provider (coreProvider/
	// prototypeProvider): renderers besides the update batch's are free to
	// ignore it entirely.
	ResultLines []string
}

ActionOutcome is what the TUI status line renders after a successful ActionProvider call. Message and each Warnings entry are TUI-facing English composed by the provider (not core, which never prints or formats for a specific caller) and are expected to be truncated to panel width by the renderer - keep them short and specific.

type ActionProgress added in v1.12.0

type ActionProgress struct {
	Line    string
	Percent float64
}

ActionProgress is one streamed progress tick from an in-flight ActionProvider mutation (ApplyInstall/ApplyUpdate/ApplyProfileSwitch - see actions_provider.go). Line is a ready-to-display, provider-composed status string kept short enough for the one-row status line (e.g. `Installing SkyUI: skyui_5_1.7z 42%`); Percent is the 0-100 completion when known, or -1 when the phase has no meaningful percentage (indeterminate - e.g. "extracting").

type ActionProvider added in v1.11.0

type ActionProvider interface {
	EnableMod(ctx context.Context, item ModItem) (ActionOutcome, error)
	DisableMod(ctx context.Context, item ModItem) (ActionOutcome, error)
	UninstallMod(ctx context.Context, item ModItem) (ActionOutcome, error)
	DeployProfile(ctx context.Context) (ActionOutcome, error)
	PlanProfileSwitch(ctx context.Context, profile string) (SwitchPlanView, error)
	// ApplyProfileSwitch's progress may be nil (see ActionProgress); it
	// streams download/install ticks when the plan being applied needs
	// them (Phase 5b Task 4 - see coreProvider/prototypeProvider's own doc
	// comments on this method).
	ApplyProfileSwitch(ctx context.Context, profile string, progress func(ActionProgress)) (ActionOutcome, error)

	// PlanInstall computes what installing item would do (files, resolved
	// dependencies, conflicts, size), without mutating anything - the
	// install-modal analog of PlanProfileSwitch.
	PlanInstall(ctx context.Context, item ModItem) (InstallPlanView, error)
	// ApplyInstall executes the plan PlanInstall would currently compute for
	// item (coreProvider re-plans at apply time, mirroring
	// ApplyProfileSwitch's own precedent). progress may be nil.
	ApplyInstall(ctx context.Context, item ModItem, progress func(ActionProgress)) (ActionOutcome, error)
	// CheckUpdates reports available updates for every checkable installed
	// mod (pinned/local mods are never checkable - filtered by core, not
	// re-filtered here).
	CheckUpdates(ctx context.Context) (UpdatesView, error)
	// ApplyUpdate applies one update reported by CheckUpdates. progress may
	// be nil.
	ApplyUpdate(ctx context.Context, u UpdateItem, progress func(ActionProgress)) (ActionOutcome, error)
	// SetUpdatePolicy sets item's update-check policy to policy, one of
	// "notify" (default: show available updates, require approval), "auto"
	// (apply automatically), or "pin" (never update) - mapping to
	// domain.UpdateNotify/UpdateAuto/UpdatePinned respectively for
	// coreProvider. Unlike CheckUpdates/ApplyUpdate this never touches the
	// network - a local DB write - so it carries no progress callback.
	SetUpdatePolicy(ctx context.Context, item ModItem, policy string) (ActionOutcome, error)

	// SetConvertPaks toggles #221 pak-to-exmod conversion for item. A local
	// DB write like SetUpdatePolicy - no network, no hooks; the next merge
	// sync applies it.
	SetConvertPaks(ctx context.Context, item ModItem, enabled bool) (ActionOutcome, error)

	// SetLock locks item at version (""= the ref's current recorded version).
	// Metadata write on the profile ref - never touches the network or deploys;
	// convergence happens on the next profile apply/switch.
	SetLock(ctx context.Context, item ModItem, version string) (ActionOutcome, error)
	// Unlock clears the lock marker; the version record stays.
	Unlock(ctx context.Context, item ModItem) (ActionOutcome, error)
	// AvailableVersions lists the distinct versions item's source reports
	// (network). The lock picker's data source.
	AvailableVersions(ctx context.Context, item ModItem) ([]string, error)

	// CreateProfile creates a new, empty profile named name (Task 6's
	// Profiles-screen 'c' binding - see mutations.go's createProfilePrompt).
	// A name colliding with an existing profile is rejected - coreProvider
	// via ProfileManager.Create's own duplicate check, prototypeProvider
	// mirroring it defensively even though the TUI's own input-modal
	// validate closure already refuses a colliding name before this is ever
	// called.
	CreateProfile(ctx context.Context, name string) (ActionOutcome, error)
	// DeleteProfile removes profile name (Task 6's Profiles-screen 'd'
	// binding - see mutations.go's deleteSelectedProfile). Deleting the
	// currently active profile is refused - the TUI's own handler already
	// checks this synchronously before ever reaching here (a status-line
	// refusal, no modal), but every implementation repeats the guard
	// defense-in-depth, since a stale active-profile row (a refresh landed
	// between the keypress and confirm) could otherwise let it through.
	DeleteProfile(ctx context.Context, name string) (ActionOutcome, error)

	// PurgeProfile undeploys every mod currently installed in the active
	// profile (Task 7's Dashboard/Installed-Mods 'X' binding - see
	// mutations.go's purgeProfilePrompt): the TUI equivalent of `lmm purge`
	// with neither --uninstall nor --force. Mod records are preserved, only
	// marked not-deployed - matching the CLI default (coreProvider never
	// exposes --uninstall's record-deleting variant; see its own doc
	// comment). progress may be nil, like every other streaming
	// ActionProvider method.
	PurgeProfile(ctx context.Context, progress func(ActionProgress)) (ActionOutcome, error)

	// ReorderMods persists a new load order for every installed mod (Task
	// 4's J/K reorder keys on Installed Mods - see mutations.go's
	// moveSelectedMod): orderedKeys is the FULL desired order, one
	// domain.ModKey("sourceID:modID") per installed mod, exactly once - the
	// order the mods will actually deploy in (last wins file conflicts, per
	// core.OrderByProfile). A local YAML write, no network call, no hooks -
	// called SYNCHRONOUSLY by moveSelectedMod (the same documented sync
	// exception DeployedFiles carries above), not through
	// buildAction/promptAction's async confirm machinery. Outcome.Message is
	// "load order updated".
	ReorderMods(ctx context.Context, orderedKeys []string) (ActionOutcome, error)

	// Rollback reverts item to its PreviousVersion (Task 6's '<' binding on
	// Installed Mods - see mutations.go's rollbackSelectedMod), wired onto
	// core.Service.ApplyRollback (Phase 6b Task 5's extraction of
	// cmd/lmm/update.go's doUpdateRollback). A mod with no PreviousVersion
	// is refused SYNCHRONOUSLY by the TUI, on the status line, before this
	// is ever called - mirroring DeleteProfile's active-profile guard's own
	// "status-line refusal, no modal" shape (see that method's doc comment)
	// - but every implementation repeats the guard defense-in-depth, exactly
	// like DeleteProfile's own coreProvider/prototypeProvider methods do.
	// progress may be nil, like every other streaming ActionProvider method.
	Rollback(ctx context.Context, item ModItem, progress func(ActionProgress)) (ActionOutcome, error)

	// PlanImport parses data (an exported profile - Phase 6b Task 9's 'I'
	// binding on Profiles, see mutations.go's importProfilePrompt) and
	// categorizes its mods against the session's current game/DB/cache
	// state, without saving anything - the import-modal analog of
	// PlanProfileSwitch/PlanInstall. data is the raw bytes the TUI already
	// read from disk (os.ReadFile, mutations.go) - this method never touches
	// the filesystem itself.
	PlanImport(ctx context.Context, data []byte) (ImportPlanView, error)
	// ApplyImport re-plans data (mirroring ApplyProfileSwitch/ApplyInstall's
	// own re-plan-at-apply precedent - see either's doc comment) and applies
	// it: the profile is saved (overwriting an existing same-named one is
	// always allowed - the preview modal's own confirm IS the TUI's only
	// overwrite consent, unlike the CLI's separate --force flag) and every
	// pending mod is downloaded and installed unconditionally (no CLI-style
	// --no-install/prompt-decline equivalent exists in the TUI). progress
	// may be nil, like every other streaming ActionProvider method.
	ApplyImport(ctx context.Context, data []byte, progress func(ActionProgress)) (ActionOutcome, error)

	// ExportProfile writes profile name's exported bytes (the same format
	// ProfileManager.Export/`lmm profile export` already produce) to path
	// (Phase 6b Task 10's 'E' binding on Profiles - see mutations.go's
	// exportProfilePrompt): a local filesystem write, no network call - the
	// same documented sync exception ReorderMods/DeployedFiles carry (see
	// ReorderMods' own doc comment), called SYNCHRONOUSLY by
	// resolveExportSubmitted rather than through buildAction/promptAction's
	// async confirm machinery. A pre-existing file at path is refused rather
	// than silently overwritten - coreProvider's own doc comment gives the
	// exact mechanism and error wording. Outcome.Message is `exported "<name>"
	// to <path>`.
	ExportProfile(ctx context.Context, name, path string) (ActionOutcome, error)

	// RunHealthCheck runs the verify engine on demand: full=true adds the
	// network version pass ('c'); fix=true applies CLI --fix semantics
	// behind the Health screen's confirmation ('F', always full). progress
	// receives one line per VerifyEvProgress / RepairDetail / Finding event.
	RunHealthCheck(ctx context.Context, full, fix bool, progress func(ActionProgress)) (HealthView, error)
}

func NewCoreActions added in v1.11.0

func NewCoreActions(svc *core.Service, game *domain.Game, profileName string) ActionProvider

NewCoreActions returns an ActionProvider backed by the real app service, for the same (svc, game, profileName) triple NewCoreProvider takes. The two constructors are independent (coreProvider carries no in-memory-only state - every mutation goes through svc's DB/filesystem, so two separate instances always observe the same underlying truth), so a caller (Task 6/7's cmd/lmm/tui.go) can call both with the game/profile it already resolved once, without re-deriving anything.

type ConflictItem added in v1.14.0

type ConflictItem struct {
	Path   string
	Owner  string
	Winner string
	AlsoIn []string
	Stale  bool
}

ConflictItem is one renderable file-conflict row for the Conflicts screen (Task 3): Owner/Winner/AlsoIn carry display NAMES only, mirroring core.ConflictModRef's own Name field (which already falls back to Key when a mod has no installed record supplying a name - see that type's doc comment) - callers never need the raw source:mod key here, only what to show. Stale mirrors core.ProfileConflict.Stale: true means the DB's recorded owner disagrees with the load-order winner (the profile was reordered, or deploy order was once nondeterministic, since the last deploy) - a redeploy would change who wins.

type DataProvider

type DataProvider interface {
	// Overview returns the dashboard summary and installed-mod rows from a
	// single underlying fetch.
	Overview(ctx context.Context) (Summary, []ModItem, error)
	Profiles(ctx context.Context) ([]ProfileItem, error)
	// Sources lists the game's configured real source IDs, sorted. The TUI
	// prepends the all-sources sentinel ("") ahead of these (see
	// newSearchModel); the CLI instead defaults to an aggregate search
	// across all of them when --source is omitted (see doSearch in
	// cmd/lmm/search.go).
	Sources() []string
	// SourceInfos lists sources registered with the service for the
	// read-only Sources screen, sorted by ID: with all == false (the
	// screen's default view - Task 4, #75), only the active game's
	// configured+registered sources (mirroring Sources(), but with full
	// display columns); with all == true, EVERY registered source
	// (built-in and user-defined), each marked SourceInfo.InUse when it
	// belongs to the active game - the 'a' toggle's full-registry view. See
	// SourceInfo's doc comment for how this differs from Sources.
	SourceInfos(all bool) []SourceInfo
	// Search queries one source, or every one of the game's configured
	// sources when source is "" (the documented all-sources sentinel).
	// pageSize is the number of results to fetch for this page (#111 Tier
	// 1's window-sized fetch - see Model.searchFetchSize in app.go
	// for how the TUI derives it); implementations fall back to
	// SearchPageSize when pageSize <= 0, a defensive default for callers
	// that don't derive a real one (see SearchPageSize's own doc comment).
	Search(ctx context.Context, source, query string, page, pageSize int) (SearchPage, error)
	// DeployedFiles lists the relative paths a specific installed mod has
	// deployed into the game directory, sorted, for the read-only files
	// overlay (Task 4). An empty slice with a nil error means the mod is
	// known but has nothing currently deployed (e.g. disabled).
	DeployedFiles(sourceID, modID string) ([]string, error)
	// ListGames lists every game configured for this session's underlying
	// app data, sorted by Name, for the in-TUI game switcher (Task 8's 'g'
	// binding - see mutations.go's openGameSwitcher). Exactly one entry has
	// Active set: the game this session is currently bound to.
	ListGames() ([]GameInfo, error)
	// Conflicts lists every file conflict the active profile currently has
	// (Task 3), sorted by Path - mirroring core.GetProfileConflicts' own
	// contract, which every implementation of this method is expected to
	// delegate to (directly, for coreProvider, or via canned data for
	// prototypeProvider). Fetched alongside Overview/Profiles in the same
	// loadData refresh cycle (app.go), not gated behind an explicit user
	// action the way Updates/CheckUpdates is: detection is local-only (DB
	// reads plus a directory walk of each enabled mod's cache - no network),
	// so it rides every ordinary load. The walks scale with installed-mod
	// count and cache size; if refreshes ever feel slow on very large mod
	// sets, memoizing per (mod, version) manifest is the obvious lever.
	Conflicts(ctx context.Context) ([]ConflictItem, error)
	// Health runs the LOCAL verify tier (disk/DB only - never the network;
	// core.VerifyLocal) for the dashboard signal and the Health screen's
	// initial content. Rides loadData like Conflicts.
	Health(ctx context.Context) (HealthView, error)
	// GetModDetails fetches full metadata for item's mod and joins local
	// install state. A network call for remote sources - callers must run it
	// off the render path (see mutations.go's openSelectedModDetails).
	GetModDetails(ctx context.Context, item ModItem) (ModDetails, error)
}

DataProvider is the narrow, read-only boundary between the TUI and app data. Implementations must be safe to call from a Bubble Tea command goroutine.

func NewCoreProvider

func NewCoreProvider(svc *core.Service, game *domain.Game, profileName string) DataProvider

NewCoreProvider returns a DataProvider backed by the real app service for one game/profile pair.

func NewPrototypeProvider

func NewPrototypeProvider() DataProvider

NewPrototypeProvider returns the side-effect-free demo DataProvider used by --prototype mode and tests. The returned value also implements ActionProvider (see actions_provider.go's prototypeProvider methods): a caller that needs both roles for one demo session should type-assert the single returned value (`provider.(ActionProvider)`) rather than calling this constructor twice, since each call seeds an independent in-memory dataset - two calls would silently diverge instead of sharing state.

type GameInfo added in v1.13.0

type GameInfo struct {
	ID, Name string
	Active   bool
}

GameInfo is one renderable configured-game row for the in-TUI game switcher (Task 8's 'g' binding - see mutations.go's openGameSwitcher). Mirrors ProfileItem's shape: just enough to render a picker option and mark which one is currently bound to the session.

type HealthFinding added in v1.30.0

type HealthFinding struct {
	ModID, ModName, FileID, Status, Note string
	Recorded, Effective, Version         string
}

HealthFinding is one renderable row from a verify run, mirroring core.VerifyFinding's shape exactly (#224 Task 8) - a thin TUI-facing copy rather than a reuse of the core type, matching every other DataProvider render model in this file (ConflictItem/SwitchPlanView/etc.) that keeps its own copy instead of exposing internal/core types across the provider boundary. Recorded/Effective/Version mirror core.VerifyFinding's identically-named additive fields (TUI layout rework, #224 follow-up): Recorded/Effective for a version_mismatch row, Version for a missing row - feeding the Health screen's VERSION column (app.go's healthFindingVersion).

type HealthView added in v1.30.0

type HealthView struct {
	Findings         []HealthFinding
	Issues, Warnings int
	Full             bool // true when produced by the Full (network) tier
	// Checked mirrors core.VerifyResult.Checked - the number of rows the
	// verify engine considered this run, feeding the Health header's "N
	// checked" suffix (healthScanLabel, app.go). Additive (#224 smoke
	// feedback, 2026-08-07).
	Checked int
}

HealthView is DataProvider.Health/ActionProvider.RunHealthCheck's result: the dashboard signal (coreProvider.Health, Local tier) and the Health screen's content (coreProvider.RunHealthCheck, Local or Full tier, dry-run or --fix).

2026-08-07 smoke feedback (user override, #224): Findings used to drop quiet-ok rows (Status "ok" with an empty Note) as "nothing to show" - but that left a healthy profile's Health screen rendering only a bare "last scan"/"no findings" pair, with no indication of what was actually checked, unlike the CLI's `lmm verify`, which prints a `+ <name> - OK` row per checked file. Findings now KEEPS every row the verify engine reports, quiet-ok included - see healthView's own doc comment in service_core.go.

type ImportPlanView added in v1.14.0

type ImportPlanView struct {
	Name, GameID                      string
	Installed, NeedsDownload, Missing []string
	Exists                            bool // a profile with this name is already saved for the game
}

ImportPlanView is the render model for the import preview modal, mapped from core.ImportPlan (see coreProvider's importPlanView) or computed directly from prototype demo data - the import-modal analog of SwitchPlanView/InstallPlanView. Mod entries are formatted "sourceID:modID vVersion", matching the CLI's own profile-import preview list lines (cmd/lmm/profile.go's doProfileImport).

type InstallPlanView added in v1.12.0

type InstallPlanView struct {
	Name, Version, Source string
	Files                 []string // display labels of the file(s) that would be downloaded
	Dependencies          []string // display names of resolved, not-yet-installed dependencies that would also install
	Conflicts             []string // "path (owned by <mod-id>)", one per conflicting file
	MissingDependencies   []string // "sourceID:modID" refs that couldn't be resolved - warn, don't block
	CycleWarning          bool     // a circular dependency was found among Dependencies; install order is best-effort
	// DependencyWarnings mirrors core.InstallPlan.DependencyWarnings (#52
	// item 10): one message per GetDependencies failure that was NOT just
	// "this source lacks the capability" - a real fetch failure the plan
	// degraded past rather than failed on. Warn, don't block, same as
	// MissingDependencies/CycleWarning.
	DependencyWarnings []string
	Reinstall          bool   // item is already installed - applying replaces it rather than installing fresh
	SizeLabel          string // "12.3 MiB", or "size unknown" when no selected file declares a size
}

InstallPlanView is the render model for the install confirmation modal, mapped from core.InstallPlan (see coreProvider's installPlanView) or computed directly from prototype demo data - the install-modal analog of SwitchPlanView.

type InstalledDetails added in v1.30.0

type InstalledDetails struct {
	Version       string
	Profile       string
	UpdatePolicy  string
	Locked        bool
	LockedVersion string
	ConvertPaks   *bool // nil = not applicable, not "off"
}

InstalledDetails mirrors core.InstalledDetail with the policy already rendered to a display string - the TUI has no reason to carry a domain.UpdatePolicy. A separate type because a view model is a rendering contract, the convention every other TUI row type follows.

type KeyMap

type KeyMap struct {
	Quit          key.Binding
	Help          key.Binding
	NextScreen    key.Binding
	PrevScreen    key.Binding
	Up            key.Binding
	Down          key.Binding
	Search        key.Binding
	SearchScreen  key.Binding
	Dashboard     key.Binding
	InstalledMods key.Binding
	Profiles      key.Binding
	Sources       key.Binding
	// HealthScreen is Task 9's direct-jump binding to ScreenHealth (#224) -
	// no other entry point reaches it directly. Task 15 folded the former
	// standalone ScreenConflicts screen (and its own "6" ConflictsScreen
	// binding) into ScreenHealth's table, moving this binding from "7" to
	// "6" - the digit now names the screen's new slot 6 position (see
	// navigation.go's screens slice).
	HealthScreen  key.Binding
	Select        key.Binding
	Submit        key.Binding
	Blur          key.Binding
	NextPage      key.Binding
	PrevPage      key.Binding
	CycleSource   key.Binding
	ConfirmAction key.Binding
	CancelAction  key.Binding
	// ToggleEnable, Uninstall, and Deploy are Phase 5a's Installed
	// Mods/Dashboard mutation bindings (see mutations.go). Profile switch
	// deliberately has no binding of its own here - it reuses Select
	// ("enter"), dispatched by screen in updateKey.
	ToggleEnable key.Binding
	Uninstall    key.Binding
	Deploy       key.Binding
	// Install is Phase 5b's install-from-search binding (see mutations.go's
	// installSelectedSearchResult): it only fires on ScreenSearch with the
	// input blurred and a result selected - a focused input swallows "i" as
	// a typed character (see updateKey's focused-input branch, which runs
	// before this ever reaches the outer switch).
	Install key.Binding
	// CheckUpdates is Phase 5b's check/apply-updates binding (see
	// mutations.go's checkForUpdates): fires on ScreenDashboard and
	// ScreenInstalledMods.
	CheckUpdates key.Binding
	// Files is Task 4's deployed-files-overlay binding (see mutations.go's
	// showDeployedFiles): fires on ScreenInstalledMods with a mod selected.
	// "f" is overloaded - overlay.go's updateOverlayKey also matches a plain
	// "f" to CLOSE the overlay once open, so this key doubles as an open/
	// close toggle.
	Files key.Binding
	// Policy is Task 5's update-policy picker binding (see mutations.go's
	// editSelectedModPolicy): fires on ScreenInstalledMods with a mod
	// selected, opening a notify/auto/pin picker whose selection dispatches
	// immediately (no separate confirm modal).
	Policy key.Binding
	// CreateProfile and DeleteProfile are Task 6's Profiles-screen bindings
	// (see mutations.go's createProfilePrompt/deleteSelectedProfile).
	// CreateProfile opens the "new profile" input modal, whose submit
	// dispatches immediately (no separate confirm modal - mirroring Policy's
	// own "the choice IS the confirmation" shape). DeleteProfile opens the
	// standard y/n confirmation modal for a non-active row, or refuses
	// synchronously on the status line for the active one.
	CreateProfile key.Binding
	DeleteProfile key.Binding
	// Purge is Task 7's Dashboard/Installed-Mods purge-behind-confirmation
	// binding (see mutations.go's purgeProfilePrompt): undeploys every mod
	// currently installed in the active profile, behind the standard y/n
	// confirmation modal - the TUI equivalent of `lmm purge`. Capital "X"
	// (distinct from lowercase "x"/Uninstall) since purge acts on the WHOLE
	// profile, not the selected mod.
	Purge key.Binding
	// GameSwitch is Task 8's in-TUI game switcher binding (see mutations.go's
	// openGameSwitcher): fires on ANY screen (unlike every other mutation
	// binding above, which is scoped to specific screens), opening a picker
	// of every configured game with the active one marked - picking one
	// dispatches immediately (no separate confirm modal, mirroring Policy's
	// own "the choice IS the confirmation" shape).
	GameSwitch key.Binding
	// MoveDown and MoveUp are Task 4's load-order reorder bindings on
	// Installed Mods (see mutations.go's moveSelectedMod): capital J/K
	// (shift+j/shift+k, aliased ctrl+down/ctrl+up) swap the selected mod with
	// its neighbor and persist the new order immediately - no confirm modal
	// (see that method's own doc comment for why). Deliberately distinct from
	// the lowercase j/k list-navigation bindings (Up/Down above); both remain
	// fully functional side by side.
	MoveDown key.Binding
	MoveUp   key.Binding
	// Rollback is Task 6's Installed-Mods rollback binding (see mutations.go's
	// rollbackSelectedMod): fires on ScreenInstalledMods with a mod selected,
	// behind the standard y/n confirmation modal - the TUI equivalent of
	// `lmm update rollback <mod-id>`. A mod with no PreviousVersion is refused on
	// the status line instead (no modal). "<" reads as "go back a version",
	// distinct from every other single-letter/shift-letter binding above.
	Rollback key.Binding
	// Changelog is Task 7's changelog-viewer binding (see actions.go's
	// updatePendingActionKey/openChangelogFromUpdateModal): fires ONLY while
	// the apply-updates confirmation modal is pending (m.pendingUpdates !=
	// nil) - a single update opens its changelog overlay directly, two or
	// more open a "pick one" picker first. Unlike ConfirmAction/CancelAction,
	// this has no meaning on any ordinary screen, so it carries no outer
	// updateKey switch case of its own.
	Changelog key.Binding
	// ImportProfile is Phase 6b Task 9's Profiles-screen import binding (see
	// mutations.go's importProfilePrompt): opens the "path to yaml" input
	// modal. Capital "I" (distinct from lowercase "i"/Install, Phase 5b's
	// install-from-search binding, which fires only on ScreenSearch) -
	// mirroring CreateProfile/DeleteProfile/Purge's own "a capital letter
	// distinguishes a Profiles/whole-profile action from an unrelated
	// lowercase one" convention.
	ImportProfile key.Binding
	// ExportProfile is Phase 6b Task 10's Profiles-screen export binding (see
	// mutations.go's exportProfilePrompt): fires on ScreenProfiles with a
	// profile row selected, opening the "path to save" input modal prefilled
	// with a default filename - submitting writes the file immediately (no
	// separate confirm modal, mirroring ImportProfile's own submit-dispatches-
	// immediately shape). Capital "E" - ImportProfile's own "a capital letter
	// distinguishes a Profiles/whole-profile action" convention, distinct from
	// any lowercase binding.
	ExportProfile key.Binding
	// ToggleAllSources is Task 4's Sources-screen scope toggle (#75, see
	// mutations.go's toggleSourcesAll): fires ONLY on ScreenSources,
	// flipping between the game-scoped default list and the full registry
	// (with the InUse marker) - the choice IS the new state, dispatching
	// immediately like Policy/GameSwitch above, no confirm modal. Lowercase
	// "a" - unlike the ExportProfile-style capital-letter "whole resource"
	// convention, this isn't a mutation at all (read-only view state), so it
	// follows the plain-lowercase pattern ToggleEnable/CycleSource use for
	// other single-screen, non-destructive toggles.
	ToggleAllSources key.Binding
	// Lock is Task 7's lock/unlock version-picker binding (#97, see
	// mutations.go's editSelectedModLock): fires on ScreenInstalledMods with a
	// mod selected, dispatching an async fetch (ActionProvider.
	// AvailableVersions - a network call, unlike Policy's synchronous fixed
	// three-option picker) whose result opens a picker of that mod's versions
	// - picking one locks at it immediately (no separate confirm modal,
	// mirroring Policy/GameSwitch's own "the choice IS the confirmation"
	// shape); a locked mod's picker additionally offers a trailing "unlock"
	// row. Capital "L" - lowercase "l" is already NextScreen's alias.
	Lock key.Binding
	// ConvertToggle toggles #221 pak-to-exmod conversion for the selected
	// mod on the Installed Mods screen. Lowercase per the single-screen
	// non-destructive-toggle convention (see ToggleEnable's "e").
	ConvertToggle key.Binding
	// FullCheck is Task 11's Health-screen full (network) verify binding
	// (#224, see mutations.go's runFullHealthCheck): fires on ScreenHealth
	// with no pushed context content, dispatching RunHealthCheck's Full tier
	// behind the standard single-flight action machinery - no confirm modal
	// (it mutates nothing), mirroring CheckUpdates' own "fetch, don't
	// confirm" shape. Deliberately reuses "c", already CreateProfile's key
	// on ScreenProfiles - see updateKey's own doc comment on how the two
	// coexist without collision (the screens never overlap).
	FullCheck key.Binding
	// FixHealth is Task 12's Health-screen batch-fix binding (#224, see
	// mutations.go's fixHealthPrompt): opens the standard confirmation modal
	// summarizing what a fix pass will attempt (counts by category), then
	// runs RunHealthCheck's Full tier with fix=true - ALWAYS the Full tier,
	// mirroring CLI --fix parity (the version pass is part of --fix, not an
	// optional extra). Capital "F" - unlike FullCheck's "c", no other screen
	// claims it, so no compound guard is needed in updateKey's switch (the
	// screen/pushed-context checks live inside fixHealthPrompt itself, like
	// every other non-colliding binding's handler).
	FixHealth key.Binding
}

KeyMap documents the TUI keyboard contract.

func DefaultKeyMap

func DefaultKeyMap() KeyMap

DefaultKeyMap returns the shared key bindings shown in help and used by tests.

type Layout

type Layout string

Layout describes the major panel arrangement for a theme.

const (
	LayoutPartySheet         Layout = "party-sheet"
	LayoutMonochromeTerminal Layout = "monochrome-terminal"
	LayoutCommander          Layout = "commander"
	LayoutCrtStack           Layout = "crt-stack"
)

type ModDetails added in v1.30.0

type ModDetails struct {
	ID, Name, Version, Author string
	Summary, Description      string
	Category                  string
	SourceURL, PictureURL     string
	Endorsements              int64
	HasEndorsements           bool

	// Installed is nil when the mod is not installed in the active profile,
	// matching `lmm mod show`'s omit rule.
	Installed *InstalledDetails

	// Fetching/FetchErr are set by the model's handler and resolvers, never
	// by a provider; the view reads them to pick its render state.
	Fetching bool
	FetchErr string
}

ModDetails is the mod-details view's render model (#86). Seeded locally from the ModItem the user selected, then enriched in place by GetModDetails - so the view opens instantly and fills in, rather than blocking on a network round trip the user may not be able to complete.

type ModItem

type ModItem struct {
	ID              string
	Name            string
	Author          string
	Version         string
	Source          string
	Status          string
	Summary         string
	Downloads       int64
	Endorsements    int64
	HasEndorsements bool
	// UpdatePolicy is the mod's current update-check policy - "notify",
	// "auto", or "pin" (see ActionProvider.SetUpdatePolicy's doc comment for
	// what each means) - populated by coreProvider's Overview mapping
	// (stringified from domain.InstalledMod.UpdatePolicy) and
	// prototypeProvider's canned Mod.UpdatePolicy field. Empty for a
	// Search-derived ModItem (search results aren't installed, so they have
	// no policy of their own) - only Overview/the Installed Mods screen ever
	// populates it.
	UpdatePolicy string
	// PreviousVersion is the version this mod would roll back to via '<' on
	// Installed Mods (Task 6, mutations.go's rollbackSelectedMod) - "" means
	// no previous version is available (the mod has never been updated, or
	// has already been rolled back once), which the handler refuses
	// synchronously rather than opening a modal. coreProvider populates this
	// from domain.InstalledMod.PreviousVersion (service_core.go's Overview
	// mapping); prototypeProvider from the canned Mod.PreviousVersion field
	// (see that type's doc comment). Empty for a Search-derived ModItem,
	// mirroring UpdatePolicy's own "only Overview populates it" convention
	// above.
	PreviousVersion string
	// Locked reports whether the profile ref for this mod carries `locked:
	// true` (#97) - lmm update refuses a locked mod (core.ErrModLocked, see
	// core.Service.ApplyUpdate's gate in flows.go). coreProvider's Overview
	// mapping (service_core.go) populates this from the profile YAML's
	// domain.ModReference.Locked, joined by (Source, ID) against the
	// installed-mods list it already loaded; prototypeProvider from the
	// canned Mod.Locked field. False for a Search-derived ModItem, mirroring
	// UpdatePolicy/PreviousVersion's own "only Overview populates it"
	// convention above. modFlags (app.go) gives Locked precedence over
	// UpdatePolicy == "pin" in the flags column - the "lck" flag wins the
	// slot; the mod's pin state is untouched and still visible in the P
	// picker and mod actions.
	Locked bool
	// LockedVersion is the profile ref's Version field when Locked is true -
	// the lock's target (domain.ModReference.Version's doc comment: "When
	// Locked, also the lock's target"). This is NOT necessarily the same
	// value as Version above: Version is the mod's actually-installed
	// version (from the DB record GetInstalledMods returns), while
	// LockedVersion is what the profile ref currently records as the locked
	// target - they can differ until the next profile apply/switch
	// converges them (SetLock/Unlock never deploy - see ActionProvider.
	// SetLock's doc comment). Empty whenever Locked is false, mirroring
	// PreviousVersion's own "empty means nothing to show" convention.
	LockedVersion string
	// ConvertPaks reports the #221 per-mod pak-to-exmod conversion flag -
	// populated by coreProvider's Overview mapping (domain.InstalledMod.
	// ConvertPaks) and left at the zero value in prototypeProvider (prototype.
	// Mod carries no ConvertPaks state), mirroring UpdatePolicy/Locked's own
	// "only Overview populates it" convention above. Meaningful only when
	// CompileGame is true.
	ConvertPaks bool
	// CompileGame is true when the active game's DeployMode is
	// domain.DeployCompile - gates the "m" toggle (mutations.go's
	// toggleSelectedModConvert) and the "raw" flag column (app.go's
	// modFlags): a non-compile game's ConvertPaks value has no effect, so
	// the toggle refuses synchronously rather than silently writing a flag
	// that does nothing.
	CompileGame bool
	// GameConvertPaks mirrors the active game's own convert_paks setting
	// (domain.Game.ConvertPaks) - the OTHER of the two levels #221's Pak
	// conversion (Icarus) README section documents ("either one is enough
	// to keep a pak raw"). Populated by coreProvider's Overview mapping
	// (service_core.go) from the game already in scope there, and left at
	// the zero value in prototypeProvider (modItems - service.go), mirroring
	// ConvertPaks' own "prototype.Mod carries no state for this" convention
	// immediately above. Meaningful only when CompileGame is true - app.go's
	// modFlags and service_core.go's SetConvertPaks both read it alongside
	// ConvertPaks rather than ConvertPaks alone, so a game-level
	// convert_paks: false is reflected in the TUI exactly like the CLI
	// already reflects it (cmd/lmm/mod.go's doModConvert).
	GameConvertPaks bool
	// HasPakSource reports whether this mod has at least one pak-kind
	// merge-source file (core.Service.ModHasPakMergeSource) - i.e. whether
	// ConvertPaks/GameConvertPaks have any deploy-time effect on it at all
	// (#221 round-4 fix). An exmodz-only mod's conversion flags are
	// meaningless: there is no pak to convert or leave raw. Populated by
	// coreProvider's Overview mapping (service_core.go); left at the zero
	// value in prototypeProvider (modItems - service.go), mirroring
	// ConvertPaks/GameConvertPaks' own "prototype.Mod carries no state for
	// this" convention above. Meaningful only when CompileGame is true -
	// app.go's modFlags and mutations.go's toggleSelectedModConvert both
	// require it alongside CompileGame before honoring the "raw" flag or the
	// "m" toggle, so an exmodz-only mod never shows a misleading "raw" flag
	// and never has its (meaningless) ConvertPaks flag toggled.
	HasPakSource bool
	// Profile is the active profile name this row is installed in - only
	// meaningful when InstalledRow is true (see that field's own doc
	// comment). Populated by coreProvider's Overview mapping
	// (service_core.go, from currentProfile()) and prototypeProvider's own
	// Overview (service.go's modItems, from data.Profile.Name). Empty for a
	// Search-derived ModItem, mirroring UpdatePolicy/Locked's own "only
	// Overview populates it" convention above - this is what
	// modDetailsFromItem seeds InstalledDetails.Profile from, fixing the
	// "(profile: )" blank parenthetical a details view used to show until
	// its background fetch landed (#86 review).
	Profile string
	// InstalledRow reports whether this ModItem's install-state fields -
	// Version (as the INSTALLED version, not a search hit's latest
	// upstream version), UpdatePolicy, Locked, LockedVersion, ConvertPaks,
	// Profile - were populated from genuine local install state, i.e. this
	// row came from Overview (the Installed Mods list) or its prototype
	// equivalent, never from a Search result.
	//
	// modDetailsFromItem (service.go) gates its InstalledDetails seed on
	// THIS field alone, never on Status: a Search hit for an
	// already-installed mod also reports Status == "installed"
	// (coreProvider's modsToItems, service_core.go) but carries the
	// SOURCE's latest Version, not what's actually installed, and leaves
	// UpdatePolicy/Locked/LockedVersion/Profile at their zero values (see
	// those fields' own doc comments) - trusting Status there fabricated an
	// Installed block from data that was never populated, showing a lying
	// version number and a false "Lock: none" for the whole fetch window,
	// permanently on a failed fetch (#86 review finding).
	//
	// Left at its zero value (false) is the SAFE default: any future
	// ModItem-constructing screen that forgets to set this explicitly gets
	// "no Installed block" rather than a silently fabricated one - the same
	// "leave it nil rather than invent a placeholder" principle
	// modDetailsFromItem already applies to Description/Category/URLs.
	InstalledRow bool
}

ModItem is one renderable mod row. ID, together with Source, fully addresses the mod for core mutations keyed on (sourceID, modID) - see ActionProvider.

type Model

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

Model is the root Bubble Tea model for the lmm TUI.

func NewModel

func NewModel(options Options) (Model, error)

NewModel creates the TUI model backed by the given DataProvider.

func NewPrototypeModel

func NewPrototypeModel(options Options) (Model, error)

NewPrototypeModel creates a side-effect-free TUI model backed by fake data. Provider and Actions are wired from the SAME prototypeProvider instance (see NewPrototypeProvider's doc comment), so actions confirmed through the returned Model are visible in its own subsequent reads — whatever the caller passed in either field is discarded. options.GameName is likewise discarded and derived from the canned active game (see prototypeProvider. activeGame) instead, mirroring Provider/Actions above (#58 item 5) - a caller has no real game to name in demo mode.

func (Model) CurrentScreen

func (m Model) CurrentScreen() Screen

CurrentScreen exposes the selected screen for tests.

func (Model) HelpVisible

func (m Model) HelpVisible() bool

HelpVisible exposes help overlay state for tests.

func (Model) Init

func (m Model) Init() tea.Cmd

func (Model) Layout

func (m Model) Layout() Layout

Layout exposes the active layout for tests and future visual selection UI.

func (Model) SelectedIndex

func (m Model) SelectedIndex(screen Screen) int

SelectedIndex exposes the selected row for tests.

func (Model) Update

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

func (Model) View

func (m Model) View() string

type Options

type Options struct {
	Theme    string
	Provider DataProvider
	// Actions is the write-side ActionProvider seam (see actions_provider.go).
	// Optional: a nil Actions means no mutation can be confirmed through
	// promptAction/buildAction, which is fine for tests that only exercise
	// the read-only DataProvider surface.
	Actions ActionProvider
	// Ctx seeds Model.ctx; see that field for why the context is stored
	// rather than threaded as a parameter.
	Ctx context.Context
	// NoColor mirrors the root --no-color flag and NO_COLOR env var (#91):
	// when true, NewModel pins lipgloss's process-global color profile to
	// termenv.Ascii before constructing the theme, so every style renders
	// plain text. Empirically, lipgloss resolves a Style's color profile at
	// Render() time by dereferencing the shared *Renderer the Style
	// captured at construction (see Style.Render/Renderer.ColorProfile) -
	// the pin therefore also takes effect for styles built earlier in the
	// same process - but pinning before theme construction keeps the
	// ordering obviously correct rather than relying on that render-time
	// detail.
	NoColor bool
	// GameName is the active game's display name, threaded into
	// newSearchModel (#58 item 5's wording-parity fix) so the TUI's
	// no-sources-configured diagnostic and all-sources honesty notice can
	// name the game the same way the CLI's own diagnostics do. Optional: an
	// empty GameName (every test double that doesn't set it) falls back to
	// a generic "this game" - see noSourcesConfiguredErr. NewPrototypeModel
	// ignores this field entirely and derives its own from the canned
	// active game, mirroring how it already discards a caller-supplied
	// Provider/Actions.
	GameName string
}

Options configures the TUI app.

type ProfileItem

type ProfileItem struct {
	Name     string
	Active   bool
	ModCount int
}

ProfileItem is one renderable profile row.

type Screen

type Screen int

Screen identifies a top-level TUI view.

const (
	ScreenDashboard Screen = iota
	ScreenInstalledMods
	ScreenSearch
	ScreenProfiles
	ScreenSources
	// ScreenHealth renders the health home view (a full-width
	// findings+conflicts table with a detail strip) - one screen among six,
	// no longer special-cased as the context-view host: since #86 generalized
	// contextview.go, ANY screen can host Model.pushContext's pushed content
	// (see contextContent's own doc comment), rendering it over whatever
	// screen pushed it instead of jumping the session to Health. #224 Task 15
	// folded the former standalone ScreenConflicts (slot 6) into this
	// screen's table - see healthTableRows/healthDetailPane's own doc
	// comments - so ScreenHealth occupies slot 6 (digit "6") instead of 7,
	// and there is no separate Conflicts screen anymore.
	ScreenHealth
)

func (Screen) String

func (s Screen) String() string

String returns a human-readable screen name.

type SearchPage added in v1.5.0

type SearchPage struct {
	Results    []ModItem
	Query      string
	Source     string
	Page       int // 0-based
	PageSize   int
	TotalCount int // 0 if the source doesn't report totals
	// Warnings holds per-source failures in all-sources mode, already
	// formatted for display (e.g. "<sourceID>: <err>"). Empty for
	// single-source searches.
	Warnings []string
	// Exhausted mirrors core.AggregateSearchResult.Exhausted (#58 item 1):
	// only meaningful for all-sources searches (Source == ""), where
	// TotalCount is summed across sources with independent pagination
	// cursors and therefore cannot bound a single global PageSize the way a
	// single source's TotalCount does - see roundExhausted's doc comment
	// (search.go) for how this replaces that unsafe math. Always false (its
	// zero value) for single-source searches, which keep using the
	// pre-existing TotalCount/PageSize math instead.
	Exhausted bool
	// AttemptedCount mirrors core.AggregateSearchResult.AttemptedCount (#58
	// item 3): only meaningful for all-sources searches (Source == "").
	// Zero real sources supporting search renders as a distinct "no source
	// supports search" notice instead of the ordinary zero-results copy -
	// see searchView's zero-results branch.
	AttemptedCount int
}

SearchPage is one page of search results for one source/query.

type SourceInfo added in v1.10.0

type SourceInfo struct {
	ID           string
	Name         string
	Type         string // "built-in", "directory", "manifest", or "api"
	Auth         string // "yes", "no", or "n/a" (source has no auth capability)
	Capabilities string // compact list, e.g. "search,updates"
	// InUse marks a row as one of the active game's configured sources.
	// Meaningful only when SourceInfos(true) (the full-registry view) was
	// requested - SourceInfos(false)'s game-scoped rows are all trivially
	// "in use" by construction, so this is left at its zero value (false)
	// there, mirroring cmd/lmm/source.go's sourceInfo.InUse (design §5's
	// CLI/TUI "same in-use marker" parity).
	InUse bool
}

SourceInfo is one renderable source-registry row, mirroring the columns of `lmm source list` (cmd/lmm/source.go) minus its Error column: the Sources screen only lists sources that are actually REGISTERED with the service. Source-definition load failures (a malformed YAML file, an ID collision) never produce a registered source, so they have no row here and remain a CLI-only diagnostic (`lmm source list` / `lmm source validate`).

type Summary

type Summary struct {
	GameName    string
	ProfileName string
	Installed   int
	Enabled     int
	Updates     int // -1 = unknown (no update check has run)
	Conflicts   int // -1 = unknown
	// HealthIssues and HealthWarnings are the dashboard's Health signal
	// (#224 Task 10) - the LOCAL verify tier's issue/warning counts, riding
	// on the same ordinary loadData refresh as Conflicts above (see
	// DataProvider.Health's doc comment: cheap disk/DB read, never gated
	// behind an explicit user action like Updates). -1 = unknown, mirroring
	// Conflicts' own sentinel - set on a scan failure (loadData wraps
	// DataProvider.Health in its own error capture, unlike Conflicts'
	// early-return-fails-the-whole-load pattern: a bad scan must not stop
	// the rest of the dashboard from loading) as well as before the very
	// first load ever completes.
	HealthIssues, HealthWarnings int
	// LastDeploy is the timestamp of the active profile's most recent deploy
	// (#106a's dashboard "Last deploy" row), or nil when the profile has
	// never been deployed (a truly-unknown value surfaces as an error from
	// the provider, never as nil here). Unlike Updates/Conflicts' "-1 =
	// unknown" int sentinel, nil is the natural "no value" for a *time.Time
	// and needs no separate sentinel constant.
	// coreProvider.Overview populates this from core.Service.
	// GetLastDeployTime, where nil specifically means "this profile has
	// never been deployed" (a normal state, not an error - see that method's
	// doc comment); prototypeProvider.Overview instead sets a canned recent
	// time so --prototype mode has something to show (see
	// prototype.Stats.LastDeploy's doc comment) - the alt game's minimal
	// demo set (Data.AltMods) leaves it nil, same as its Updates/Conflicts
	// sentinels. Rendering is lastDeployLabel's job (app.go): it takes the
	// current time as an explicit parameter (Model.now) rather than calling
	// time.Now() itself, so the label is recomputed fresh on every View()
	// call instead of going stale between loadData refreshes.
	LastDeploy *time.Time
}

Summary is the dashboard header data.

type SwitchPlanView added in v1.11.0

type SwitchPlanView struct {
	From, To       string
	Enable         []string // mod names to enable
	Disable        []string // mod names to disable
	NeedsDownloads []string // mod refs requiring download - ApplyProfileSwitch downloads and installs these itself (Phase 5b Task 4; see its doc comment), streaming progress the same way ApplyInstall/ApplyUpdate do
	NoChanges      bool
	AlreadyActive  bool
}

SwitchPlanView is the render model for the profile-switch confirmation modal, mapped from core.SwitchPlan (see coreProvider's switchPlanView) or computed directly from prototype demo data.

type UpdateItem added in v1.12.0

type UpdateItem struct {
	Source, ID, Name       string
	FromVersion, ToVersion string
	// Changelog is the update's changelog, already run through
	// core.CleanChangelog (Phase 6b Task 7) - the FULL cleaned text, with NO
	// truncation: unlike cmd/lmm/update.go's own 800/500-char CLI
	// truncation (a presentation concern that stays CLI-side), the TUI's
	// changelog overlay (actions.go's openChangelogFromUpdateModal) shows
	// the whole thing, scrollable (see infoOverlay.offset) so every line is
	// reachable however long it runs. Empty means the source reported none -
	// the overlay renders "no changelog available" rather than an empty
	// panel.
	Changelog string
	// Locked/LockedVersion project the profile ref's lock state onto the
	// update row (#143): a locked mod is still checked and reported ("locked
	// but informed", the same contract as the CLI's bulk table), but
	// ApplyUpdate will refuse it, so the batch-apply modal marks the row up
	// front instead of letting the refusal surprise the user after confirm.
	// LockedVersion follows ModItem.LockedVersion's "empty whenever Locked
	// is false" contract.
	Locked        bool
	LockedVersion string
	// RecompileNeeded marks a #197 merged-pak staleness row (generalizing
	// #196's per-mod version): the profile's merged pak no longer matches
	// its recorded fingerprint (enabled-mod set, load order, a mod's
	// version, or the base pak changed). ToVersion equals FromVersion in
	// this case - u itself is the SYNTHETIC merged-pak row, not a real
	// installed mod - and ApplyUpdate routes such a row to
	// Service.ApplyMergedPakRegen instead of Service.ApplyUpdate.
	RecompileNeeded bool
	// RecompileReason is domain.Update's own distinct staleness reason
	// ("base pak updated" - the fingerprint changed - or "not deployed" -
	// the fingerprint still matches but the artifact is missing from the
	// game directory), meaningful only when RecompileNeeded is true. #203
	// release review: the TUI used to hardcode "(base pak updated)" for
	// every staleness row regardless of the real cause, unlike `lmm verify`
	// (cmd/lmm/verify.go), which already names the distinct reason.
	RecompileReason string
}

UpdateItem is one available update, as reported by CheckUpdates and consumed by ApplyUpdate.

func (UpdateItem) VersionLabel added in v1.28.0

func (u UpdateItem) VersionLabel() string

VersionLabel renders u's version change for display: the normal "<from> → <to>" arrow for a real update, or "(<reason>)" for a #197 RecompileNeeded row, where FromVersion == ToVersion and an arrow would misleadingly read as a no-op - RecompileReason names the actual cause ("base pak updated" or "not deployed"), matching what `lmm verify` already shows; an empty RecompileReason (defensive - core always sets one alongside RecompileNeeded) falls back to "base pak updated" rather than rendering an empty "()". Used everywhere an UpdateItem's version change is shown - the apply-updates modal, its result lines, and the changelog picker/overlay - so all of them read sanely for a staleness row without duplicating this branch four times.

type UpdatesView added in v1.12.0

type UpdatesView struct {
	Updates  []UpdateItem
	Warnings []string
}

UpdatesView is CheckUpdates' result: the available updates plus any non-fatal per-source diagnostics (partial results still populate Updates - see coreProvider.CheckUpdates' doc comment).

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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