tui

package
v1.13.1 Latest Latest
Warning

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

Go to latest
Published: Jul 23, 2026 License: MIT Imports: 20 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
}

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

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

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

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
}

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.

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
}

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
)

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
}

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
}

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
}

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