tui

package
v1.18.1 Latest Latest
Warning

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

Go to latest
Published: Jul 27, 2026 License: MIT Imports: 22 Imported by: 0

Documentation

Index

Constants

View Source
const SearchPageSize = 10

SearchPageSize mirrors the CLI picker's displayPageSize (cmd/lmm/install.go).

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

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)

	// 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

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 every source registered with the service (built-in
	// and user-defined), sorted by ID, for the read-only Sources screen. See
	// SourceInfo's doc comment for how this differs from Sources.
	SourceInfos() []SourceInfo
	// Search queries one source, or every one of the game's configured
	// sources when source is "" (the documented all-sources sentinel).
	Search(ctx context.Context, source, query string, page 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

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 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
}

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 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
}

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

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 hasNextPage's doc comment 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"
}

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
}

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

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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