package tui import ( "context" "strings" "time" "charm.land/bubbles/v2/list" "charm.land/bubbles/v2/textinput" "charm.land/bubbles/v2/viewport" tea "charm.land/bubbletea/v2" "charm.land/lipgloss/v2" "gitgud.io/mike/mpv-manager/pkg/config" "gitgud.io/mike/mpv-manager/pkg/hotkeys" "gitgud.io/mike/mpv-manager/pkg/installer" "gitgud.io/mike/mpv-manager/pkg/locale" "gitgud.io/mike/mpv-manager/pkg/modernzconf" "gitgud.io/mike/mpv-manager/pkg/platform" "gitgud.io/mike/mpv-manager/pkg/uiconfig" "gitgud.io/mike/mpv-manager/pkg/uoscconf" "gitgud.io/mike/mpv-manager/pkg/version" ) const ( banner = " ▶ MPV.Rocks" ) var ( purple = lipgloss.Color("#9B59B6") lightPurple = lipgloss.Color("#BB8FCE") white = lipgloss.Color("#FFFFFF") black = lipgloss.Color("#000000") gray = lipgloss.Color("#95A5A6") green = lipgloss.Color("#2ECC71") blue = lipgloss.Color("#085dd3") red = lipgloss.Color("#E74C3C") yellow = lipgloss.Color("#F1C40F") orange = lipgloss.Color("#E67E22") bannerStyle = lipgloss.NewStyle(). Foreground(white). Background(purple). Bold(true) titleStyle = lipgloss.NewStyle(). Foreground(purple). Bold(true) subtitleStyle = lipgloss.NewStyle(). Foreground(lightPurple) infoStyle = lipgloss.NewStyle(). Foreground(gray) successStyle = lipgloss.NewStyle(). Foreground(green) errorStyle = lipgloss.NewStyle(). Foreground(red) warningStyle = lipgloss.NewStyle(). Foreground(yellow) helpStyle = lipgloss.NewStyle(). Foreground(gray) selectedItemStyle = lipgloss.NewStyle(). Foreground(green). Bold(true) footerStyle = lipgloss.NewStyle(). Foreground(gray) linkStyle = lipgloss.NewStyle(). Foreground(blue). Underline(true) recommendedBadgeStyle = lipgloss.NewStyle(). Foreground(blue). Bold(true) av2Style = lipgloss.NewStyle().Foreground(red).Bold(true) vvcStyle = lipgloss.NewStyle().Foreground(blue).Bold(true) av1Style = lipgloss.NewStyle().Foreground(yellow).Bold(true) vp9Style = lipgloss.NewStyle().Foreground(green).Bold(true) hevcStyle = lipgloss.NewStyle().Foreground(blue).Bold(true) vp8Style = lipgloss.NewStyle().Foreground(green).Bold(true) proresStyle = lipgloss.NewStyle().Foreground(purple).Bold(true) vc1Style = lipgloss.NewStyle().Foreground(orange).Bold(true) avcStyle = lipgloss.NewStyle().Foreground(red).Bold(true) mpeg2Style = lipgloss.NewStyle().Foreground(purple).Bold(true) ) var codecDisplayOrder = []string{"av2", "vvc", "av1", "vp9", "hevc", "vp8", "prores", "vc1", "avc", "mpeg2"} var codecDisplayNames = map[string]string{ "av2": "AV2", "vvc": "VVC", "av1": "AV1", "vp9": "VP9", "hevc": "HEVC", "vp8": "VP8", "prores": "ProRes", "vc1": "VC-1", "avc": "AVC", "mpeg2": "MPEG-2", } var codecStyles = map[string]lipgloss.Style{ "av2": av2Style, "vvc": vvcStyle, "av1": av1Style, "vp9": vp9Style, "hevc": hevcStyle, "vp8": vp8Style, "prores": proresStyle, "vc1": vc1Style, "avc": avcStyle, "mpeg2": mpeg2Style, } type State int const ( StateMainMenu State = iota StateInstallMenu StateInstalling StateSuccess StateError StateUpdateCheck StateUpdateInstaller StateUpdates StateUpdateList StateRestoreConfig StateUninstall StateInstallToPath StateLanguagePreferences StateLanguageMajorSelect StateLanguageRegionSelect StateLanguageSelectedList StateLanguageConfirm StateConfigOptions StateHWAOptions StateScreenshotOptions StateFileAssociations StateFileAssociationsMenu StateWebUIMenu // Screenshot editing sub-states StateScreenshotFormatEdit StateScreenshotTagColorspaceEdit StateScreenshotHighBitDepthEdit StateScreenshotTemplateEdit StateScreenshotDirectoryEdit StateScreenshotJpegQualityEdit StateScreenshotPngCompressionEdit StateScreenshotWebpLosslessEdit StateScreenshotWebpQualityEdit StateScreenshotJxlDistanceEdit StateScreenshotAvifEncoderEdit StateScreenshotAvifPixfmtEdit StateScreenshotAvifOptsEdit // Additional config editing sub-states StateSavePositionOnQuitEdit // Hotkeys view StateHotkeys StateHotkeysCategory StateHotkeysPreset // Keybinding preset selection StateUIAppSelect // Exact installed MPV app selection for UI changes StateUISelect // UI overlay selection state // Hotkey bindings editor StateHotkeysBindings // Current bindings list StateHotkeysBindingEdit // Editing a binding's command StateHotkeysBindingAddKey // Adding a binding: key name input StateHotkeysBindingAddCmd // Adding a binding: command input // ModernZ options editor StateModernZOptions StateModernZOptionEdit StateUOSCOptions StateUOSCOptionEdit // Custom install path StateInstallPath StateInstallPathEdit // Job history and logs viewers StateJobHistory StateJobHistoryDetail StateLogsViewer // Reset MPV Manager data confirmation StateResetManagerData ) type InstallOperationType int const ( OpTypeNone InstallOperationType = iota OpTypeInstall OpTypeUpdate OpTypeConfig OpTypeManagerUpdate OpTypeUIChange ) type Model struct { state State platform PlatformInfo platformInfo *platform.Platform installer *installer.Installer methods []installer.InstallMethod menuList list.Model installMenuList list.Model updateList list.Model uninstallList list.Model restoreConfigList list.Model majorLangList list.Model regionLangList list.Model selectedLangList list.Model langPreferenceList list.Model configOptionsList list.Model hwaOptionsList list.Model screenshotOptionsList list.Model fileAssociationsList list.Model webUIList list.Model hotkeysList list.Model hotkeysCategoryList list.Model hotkeysPresetList list.Model viewport viewport.Model outputViewport viewport.Model statusMessage string errorMessage string installProgress int showHelp bool showVersion bool selectedMethod string selectedMethodID string installing bool versionCheckResult *version.VersionCheckResult updateCheckResult *version.UpdateCheckResult releaseInfoLoader func() (*installer.ReleaseInfo, error) releaseInfoContextLoader func(context.Context) (*installer.ReleaseInfo, error) observations *tuiObservations releaseInfoLoading bool releaseInfoStatus string pendingUpdatesCheck bool installOutput []string currentCommand string installComplete bool installError error installSuccess bool loadingConfig bool autoScroll bool outputViewportDirty bool lastOutputRefresh time.Time outputChan <-chan string installDone <-chan error termWidth int termHeight int selectedUninstall *config.InstalledApp selectedUpdateItem *updateItem langPreferenceType string currentMajorLanguage *locale.LocaleEntry selectedLanguageCodes []string loadedLocales []locale.LocaleEntry currentMPVAudioLangs []string currentMPVSubtitleLangs []string updateItems []list.Item selectedRestoreBackup *version.ConfigBackupItem installerCheckType string downloadWritten int64 downloadTotal int64 progressChan <-chan downloadProgressMsg managerPreparedChan <-chan *version.PreparedSelfUpdate operationActive bool operationTerminal func(error) tea.Msg operationRead *operationRead operationCancel func() operationCommitGuard *operationCommitGuard operationResult error operationResultReceived bool operationCancelRequested bool operationReturnToMenu bool operationQuitAfter bool selectedHWAOption string installOperationType InstallOperationType managerUpdateReady bool preparedManagerUpdate *version.PreparedSelfUpdate managerExecutable func() (string, error) prepareManagerUpdate func(context.Context, string, *version.VersionCheckResult, version.SelfUpdateProgressCallback) (*version.PreparedSelfUpdate, error) launchManagerUpdate func(*version.PreparedSelfUpdate, string) error postUpdateJournal string acknowledgeSelfUpdate func(string) error installStartedAt time.Time postInstallMessage string langSearchTerm string isLangSearching bool langSearchResults []locale.LanguageOption // Screenshot editing fields selectedScreenshotOption string screenshotTextInput textinput.Model screenshotValueList list.Model // Hotkeys view fields presetStatusMessage string // Status message after preset apply presetApplyInFlight bool // UI selection fields uiSelectList list.Model uiAppList list.Model pendingMethodID string // Method waiting for UI selection pendingAppName string // App name for display selectedUIType string // Selected UI overlay type selectedUIApp *config.InstalledApp uiChangeMode bool // Hotkey bindings editor fields hotkeysBindingsList list.Model inputConf *hotkeys.InputConfig inputConfPath string bindingTextInput textinput.Model bindingStatusMsg string bindingConfirm string // "", "overwrite", or "delete" bindingEditKey string // Key being edited or deleted bindingNewKey string // Key name entered in the add flow bindingNewCmd string // Pending command for an overwrite confirmation // ModernZ options editor fields modernzList list.Model modernzConf *modernzconf.ModernZConf modernzTextInput textinput.Model modernzStatusMsg string modernzEditKey string // uOSC options editor fields uoscList list.Model uoscConf *uoscconf.UOSCConf uoscTextInput textinput.Model uoscStatusMsg string uoscEditKey string // Shared UI configuration audit and migration state. uiConfigMigration *uiconfig.Migration uiConfigWarnings []uiconfig.Warning // Custom install path fields installPathList list.Model installPathTextInput textinput.Model installPathStatusMsg string // Reset MPV Manager data confirmation status resetManagerDataStatusMsg string // Job history viewer fields jobHistoryList list.Model jobHistoryRecords []jobHistoryRecord selectedJobRecord *jobHistoryRecord jobHistoryLoadErr string jobDetailViewport viewport.Model // Logs viewer fields logsViewport viewport.Model logsStatusMsg string // Web UI menu fields webUIStatusMsg string } // hotkeyItem implements list.Item for hotkey display type hotkeyItem struct { keys string description string category string } func (h hotkeyItem) Title() string { return h.keys } func (h hotkeyItem) Description() string { return h.description + " (" + h.category + ")" } func (h hotkeyItem) FilterValue() string { return h.keys + " " + h.description + " " + h.category } func formatCodecsDisplay(codecs []string) string { if len(codecs) == 0 { return "Unknown" } var ordered []string for _, codecKey := range codecDisplayOrder { for _, detectedCodec := range codecs { if strings.ToLower(detectedCodec) == codecKey || strings.ToLower(detectedCodec) == codecKey+"265" || strings.ToLower(detectedCodec) == codecKey+"266" || strings.ToLower(detectedCodec) == "h26"+codecKey[1:] { displayName := codecDisplayNames[codecKey] style := codecStyles[codecKey] ordered = append(ordered, style.Render(displayName)) break } } } if len(ordered) == 0 { return "Unknown" } return strings.Join(ordered, " ") } func getVendorColor(brand string) lipgloss.Style { switch strings.ToLower(brand) { case "amd": return lipgloss.NewStyle().Foreground(red).Bold(true) case "nvidia": return lipgloss.NewStyle().Foreground(green).Bold(true) case "intel": return lipgloss.NewStyle().Foreground(blue).Bold(true) case "apple": return titleStyle case "arm", "armel", "qualcomm", "mediatek", "samsung", "hisilicon": return lipgloss.NewStyle().Foreground(orange).Bold(true) default: return infoStyle } }