Documentation
¶
Index ¶
- Constants
- type ActionOutcome
- type ActionProgress
- type ActionProvider
- type ConflictItem
- type DataProvider
- type GameInfo
- type ImportPlanView
- type InstallPlanView
- type KeyMap
- type Layout
- type ModItem
- type Model
- type Options
- type ProfileItem
- type Screen
- type SearchPage
- type SourceInfo
- type Summary
- type SwitchPlanView
- type UpdateItem
- type UpdatesView
Constants ¶
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. 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
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)
// 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)
}
func NewCoreActions ¶ added in v1.11.0
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
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)
}
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 ¶
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
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 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 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
// ConflictsScreen is Task 3's direct-jump binding to ScreenConflicts,
// mirroring SearchScreen's own "named screen, separate from the plain
// Dashboard/InstalledMods/Profiles/Sources bindings" shape (both name a
// specific screen a user might reach some other way too - SearchScreen
// via "/", ConflictsScreen has no such alternate entry point yet).
ConflictsScreen 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
}
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 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
}
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 NewPrototypeModel ¶
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 ¶
CurrentScreen exposes the selected screen for tests.
func (Model) HelpVisible ¶
HelpVisible exposes help overlay state for tests.
func (Model) SelectedIndex ¶
SelectedIndex exposes the selected row for tests.
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 ¶
ProfileItem is one renderable profile row.
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
// 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
}
UpdateItem is one available update, as reported by CheckUpdates and consumed by ApplyUpdate.
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).