package tui import ( "fmt" "os" "path/filepath" "runtime" "strings" "time" "gitgud.io/mike/mpv-manager/pkg/config" "gitgud.io/mike/mpv-manager/pkg/constants" "gitgud.io/mike/mpv-manager/pkg/installer" "gitgud.io/mike/mpv-manager/pkg/log" "gitgud.io/mike/mpv-manager/pkg/version" "github.com/charmbracelet/bubbles/list" "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" ) func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmd tea.Cmd switch msg := msg.(type) { case tea.KeyMsg: switch msg.Type { case tea.KeyCtrlC: m.installing = false m.selectedMethod = "" m.selectedMethodID = "" return m, tea.Quit case tea.KeyCtrlH: m.showHelp = !m.showHelp return m, nil case tea.KeyCtrlV: m.showVersion = !m.showVersion return m, nil case tea.KeyEnter: if m.state == StateMainMenu && !m.installing { selected := m.menuList.SelectedItem() if selected != nil { item := selected.(menuItem) m.selectedMethod = item.title m.selectedMethodID = item.id m.statusMessage = item.title switch m.selectedMethodID { case "updates": m.state = StateUpdates m.updateCheckResult = nil m.versionCheckResult = nil m.updateItems = []list.Item{} m.installerCheckType = "manager" return m, runUpdatesCheckCmd(m.installer) case "mpv-manager": m.setupInstallMenuList() m.state = StateInstallMenu m.statusMessage = "Select Installation Method" return m, nil case "unmpv-manager": m.state = StateUninstall return m, refreshUninstallList() case "install-to-path": m.state = StateInstallToPath case "set-language-preferences": m.loadLanguageData() m.setupLangPreferenceList() m.state = StateLanguagePreferences return m, nil case "config-options": m.setupConfigOptionsList() m.state = StateConfigOptions return m, nil case "hotkeys": m.setupHotkeysPresetList() m.state = StateHotkeysPreset m.statusMessage = "Keybinding Presets" return m, nil case "web-ui": m.setupWebUIList() m.state = StateWebUIMenu return m, nil } } } else if m.state == StateInstallMenu && !m.installing { selected := m.installMenuList.SelectedItem() if selected != nil { item := selected.(menuItem) m.selectedMethod = item.title m.selectedMethodID = item.id m.statusMessage = item.title // Check if this method needs UI selection if needsUISelection(m.selectedMethodID) { m.pendingMethodID = m.selectedMethodID m.pendingAppName = getAppNameForMethod(m.selectedMethodID) m.setupUISelectList() m.state = StateUISelect return m, nil } // Direct installation for non-MPV methods (IINA, MPC-QT, Celluloid) m.state = StateInstalling m.installProgress = 0 m.installing = true m.installComplete = false m.installOperationType = OpTypeInstall installDir := m.getInstallPath() // Use default UI type for direct installations return m, executeInstallCmd(m.selectedMethodID, installDir, true, m.platformInfo, m.installer) } } else if m.state == StateUninstall && !m.installing { selected := m.uninstallList.SelectedItem() if selected != nil { appItem := selected.(uninstallAppItem) m.selectedUninstall = appItem.app m.statusMessage = appItem.app.AppName m.state = StateInstalling m.installProgress = 0 m.installing = true m.installComplete = false return m, executeUninstallCmd(appItem.app, m.platformInfo, m.installer) } } else if m.state == StateUpdates && !m.installing { selected := m.updateList.SelectedItem() if selected != nil { switch item := selected.(type) { case menuItem: if item.id == "update-installer" { if version.SelfUpdateDisabled == "true" { // Self-update is disabled, don't enter update state return m, nil } m.state = StateUpdateInstaller return m, checkForUpdateCmd() } else if item.id == "load-config" { m.state = StateInstalling m.loadingConfig = true m.installProgress = 0 m.installing = true m.installComplete = false outputChan := make(chan string, 100) installDone := make(chan error, 1) go func() { cr := installer.NewCommandRunner(outputChan, nil) var err error err = m.installer.InstallMPVConfigWithOutput(cr) installDone <- err close(outputChan) }() return m, func() tea.Msg { return saveChannelsMsg{outputChan: outputChan, installDone: installDone} } } case updateItem: if item.UpdateType == "update" { m.state = StateInstalling m.installProgress = 0 m.installing = true m.installComplete = false m.selectedUpdateItem = &item m.installOperationType = OpTypeUpdate return m, executeUpdateCmd(item.MethodID, item.AppName, m.platformInfo, m.installer) } else if item.UpdateType == "package-update" { m.state = StateInstalling m.installProgress = 0 m.installing = true m.installComplete = false m.installOperationType = OpTypeUpdate return m, executeUpdateCmd(item.MethodID, item.AppName, m.platformInfo, m.installer) } case configBackupItem: m.selectedRestoreBackup = item.backup m.state = StateRestoreConfig items := make([]list.Item, len(m.updateCheckResult.ConfigBackupItems)) for i, backup := range m.updateCheckResult.ConfigBackupItems { items[i] = configBackupItem{backup: &backup} } m.restoreConfigList = m.createStyledList("Select Backup to Restore", items) return m, nil } } } else if m.state == StateRestoreConfig && !m.installing { selected := m.restoreConfigList.SelectedItem() if selected != nil { backupItem := selected.(configBackupItem) m.selectedRestoreBackup = backupItem.backup m.state = StateInstalling m.installProgress = 0 m.installing = true m.installComplete = false m.installOperationType = OpTypeConfig m.currentCommand = fmt.Sprintf("Restoring config from %s", m.selectedRestoreBackup.BackupName) return m, m.runRestoreConfigCmd() } } else if m.state == StateInstallToPath && !m.installing { m.state = StateInstalling m.installProgress = 0 m.installing = true m.installComplete = false m.selectedMethodID = "install-to-path" isRemoving := isInstallMPVInPATH() return m, executePathCmd(isRemoving, m.installer) } case tea.KeyEsc: if m.state == StateError { m.state = StateMainMenu m.errorMessage = "" m.installOutput = []string{} m.currentCommand = "" return m, nil } else if m.state == StateInstallMenu { m.resetToMainMenu() return m, nil } else if m.state == StateInstalling { m.resetToMainMenu() return m, nil } else if m.state == StateSuccess { m.resetToMainMenu() return m, nil } else if m.state == StateUpdateInstaller || m.state == StateUninstall || m.state == StateInstallToPath { m.resetToMainMenu() return m, nil } else if m.state == StateUpdates { m.resetToMainMenu() return m, nil } else if m.state == StateConfigOptions { m.resetToMainMenu() return m, nil } else if m.state == StateHWAOptions { m.setupConfigOptionsList() m.state = StateConfigOptions return m, nil } else if m.state == StateScreenshotOptions { m.setupConfigOptionsList() m.state = StateConfigOptions return m, nil } else if m.state == StateRestoreConfig { m.setupConfigOptionsList() m.state = StateConfigOptions return m, nil } else if m.state == StateFileAssociations { m.setupConfigOptionsList() m.state = StateConfigOptions return m, nil } else if m.state == StateFileAssociationsMenu { m.setupConfigOptionsList() m.state = StateConfigOptions return m, nil } else if m.state == StateSuccess { m.resetToMainMenu() return m, nil } else if m.state == StateUpdateInstaller || m.state == StateUpdateList || m.state == StateUninstall || m.state == StateInstallToPath { m.resetToMainMenu() m.RefreshMainMenu() return m, nil } case tea.KeyRunes: if m.state == StateUpdateInstaller && len(msg.Runes) == 1 { switch string(msg.Runes[0]) { case "u", "U": if version.SelfUpdateDisabled == "true" { // Self-update is disabled, don't allow update return m, nil } if m.versionCheckResult != nil && m.versionCheckResult.UpdateAvailable { m.state = StateInstalling m.installProgress = 0 m.installing = true m.installComplete = false m.downloadWritten = 0 m.downloadTotal = 0 m.currentCommand = "Updating MPV Manager" outputChan := make(chan string, 100) installDone := make(chan error, 1) progressChan := make(chan downloadProgressMsg, 10) go func() { execPath, err := os.Executable() if err != nil { outputChan <- fmt.Sprintf("Error: Failed to get executable path: %v", err) installDone <- err close(outputChan) close(progressChan) return } progressCallback := func(written, total int64) { progressChan <- downloadProgressMsg{written: written, total: total} } if err := version.UpdateSelfWithProgress(execPath, progressCallback); err != nil { outputChan <- fmt.Sprintf("Error: Failed to update manager: %v", err) installDone <- err } else { outputChan <- "✓ Manager updated successfully!" outputChan <- "✓ BLAKE3 hash verified." if config.GetManagerInPATH() { outputChan <- "✓ PATH installation also updated." } outputChan <- "" outputChan <- "Press ENTER to restart application..." } close(outputChan) close(progressChan) }() return m, func() tea.Msg { return saveChannelsMsg{ outputChan: outputChan, installDone: installDone, progressChan: progressChan, } } } } } } case tea.WindowSizeMsg: m.termWidth = msg.Width m.termHeight = msg.Height m.viewport.Width = msg.Width m.viewport.Height = msg.Height m.outputViewport.Width = msg.Width - 10 m.outputViewport.Height = msg.Height / 2 m.menuList.SetWidth(msg.Width - 10) m.menuList.SetHeight(msg.Height - 15) m.installMenuList.SetWidth(msg.Width - 10) m.installMenuList.SetHeight(msg.Height - 15) m.uninstallList.SetWidth(msg.Width - 10) m.uninstallList.SetHeight(msg.Height - 15) m.updateList.SetWidth(msg.Width - 10) m.updateList.SetHeight(msg.Height - 15) m.restoreConfigList.SetWidth(msg.Width - 10) m.restoreConfigList.SetHeight(msg.Height - 15) m.screenshotOptionsList.SetWidth(msg.Width - 10) m.screenshotOptionsList.SetHeight(msg.Height - 15) m.uiSelectList.SetWidth(msg.Width - 10) m.uiSelectList.SetHeight(msg.Height - 15) case installErrorMsg: m.state = StateError m.errorMessage = msg.err.Error() m.installing = false return m, nil case versionCheckMsg: m.versionCheckResult = msg.result return m, nil case updateCheckResultMsg: m.updateCheckResult = msg.result items := make([]list.Item, 0, 2+len(msg.result.AppsToUpdate)) // Only show "Update MPV Manager" option when self-update is not disabled if version.SelfUpdateDisabled != "true" { installerItem := menuItem{ id: "update-installer", title: "🔄 Update MPV Manager", } if m.versionCheckResult != nil { if m.versionCheckResult.UpdateAvailable { installerItem.title = "🔄 Update MPV Manager " + successStyle.Render("[ Update Available ]") installerItem.description = fmt.Sprintf("Current: %s - Available: %s - Select to update", m.versionCheckResult.CurrentVersion, m.versionCheckResult.LatestVersion) } else if m.versionCheckResult.Error == nil { installerItem.description = fmt.Sprintf("Current: %s - Latest", m.versionCheckResult.CurrentVersion) } else { installerItem.description = "Error checking for updates" } } else { installerItem.description = "Checking for manager updates..." } items = append(items, installerItem) } items = append(items, menuItem{ id: "load-config", title: "📥 Load Latest Recommended MPV Config", description: "Install the latest recommended mpv.conf configuration", }) for _, app := range msg.result.AppsToUpdate { items = append(items, updateItem{ AppName: app.AppName, CurrentVersion: app.CurrentVersion, AvailableVersion: app.AvailableVersion, UpdateType: app.UpdateType, PackageManager: app.PackageManager, MethodID: app.MethodID, }) } m.updateList = m.createStyledList("Available Updates", items) return m, nil case uninstallAppsLoadedMsg: items := make([]list.Item, len(msg.apps)) for i, app := range msg.apps { items[i] = uninstallAppItem{app: &app} } m.uninstallList.SetItems(items) m.uninstallList.Title = "Installed Applications" m.uninstallList.Styles.Title = titleStyle return m, nil case refreshMenuMsg: m.RefreshMainMenu() return m, tea.Tick(10*time.Millisecond, func(t time.Time) tea.Msg { return tickMsg{} }) case cmdStartMsg: m.currentCommand = msg.command m.installOutput = append(m.installOutput, "") m.installOutput = append(m.installOutput, fmt.Sprintf("Running: %s", msg.command)) m.installOutput = append(m.installOutput, strings.Repeat("─", 50)) m.updateOutputViewport() return m, nil case saveChannelsMsg: m.outputChan = msg.outputChan m.installDone = msg.installDone if msg.progressChan != nil { m.progressChan = msg.progressChan } return m, startStreaming(m.outputChan, m.installDone, m.progressChan) case downloadProgressMsg: m.downloadWritten = msg.written m.downloadTotal = msg.total if m.downloadTotal > 0 { percent := float64(m.downloadWritten) / float64(m.downloadTotal) * 100 m.installProgress = int(percent) } return m, startStreaming(m.outputChan, m.installDone, m.progressChan) case cmdOutputMsg: m.installOutput = append(m.installOutput, msg.line) m.updateOutputViewport() if m.autoScroll { m.outputViewport.GotoBottom() } return m, startStreaming(m.outputChan, m.installDone, nil) case cmdDoneMsg: m.installComplete = true m.installing = false m.loadingConfig = false m.installError = msg.err if postInstallMsg, ok := msg.err.(installer.PostInstallMessage); ok { m.postInstallMessage = postInstallMsg.Message m.installError = nil } if msg.err == nil { m.installSuccess = true // Handle successful install/update if m.selectedUninstall != nil { // ✅ SUCCESSFUL UNINSTALL log.Separator("Uninstallation Summary") log.Info(fmt.Sprintf("Uninstallation completed successfully: %s", m.selectedUninstall.AppName)) log.Info("Removed from installed_apps config") config.RemoveInstalledApp(m.selectedUninstall.AppName) m.RefreshMainMenu() // Show success message to user m.installOutput = append(m.installOutput, "") m.installOutput = append(m.installOutput, "✓ Uninstallation completed successfully!") m.installOutput = append(m.installOutput, fmt.Sprintf("Removed: %s", m.selectedUninstall.AppName)) m.installOutput = append(m.installOutput, "") m.installOutput = append(m.installOutput, "Press Enter to return to main menu or Esc to quit") m.updateOutputViewport() } else if m.selectedMethodID == "install-to-path" { log.Separator("PATH Operation Summary") binDir := "" if runtime.GOOS == "windows" { homeDir, _ := os.UserHomeDir() binDir = filepath.Join(homeDir, "bin") } else { homeDir, _ := os.UserHomeDir() binDir = filepath.Join(homeDir, ".local", "bin") } inPath := isInstallMPVInPATH() if inPath { log.Info("mpv-manager added to system PATH successfully") log.Info(fmt.Sprintf("Binary location: %s", filepath.Join(binDir, constants.AppName))) log.Info(fmt.Sprintf("Symlink location: %s", filepath.Join(binDir, "mpv-install"))) config.SetManagerInPATH(true) config.SetManagerBinPath(filepath.Join(binDir, constants.AppName)) m.installOutput = append(m.installOutput, "") m.installOutput = append(m.installOutput, "✓ mpv-manager added to system PATH successfully!") m.installOutput = append(m.installOutput, fmt.Sprintf(" Binary: %s", filepath.Join(binDir, constants.AppName))) m.installOutput = append(m.installOutput, fmt.Sprintf(" Symlink: %s", filepath.Join(binDir, "mpv-install"))) m.installOutput = append(m.installOutput, "") m.installOutput = append(m.installOutput, "You can now run 'mpv-manager' or 'mpv-install' from anywhere!") m.installOutput = append(m.installOutput, "") m.installOutput = append(m.installOutput, "Press Enter to return to main menu or Esc to quit") m.updateOutputViewport() } else { log.Info("mpv-manager removed from system PATH successfully") config.SetManagerInPATH(false) config.SetManagerBinPath("") m.installOutput = append(m.installOutput, "") m.installOutput = append(m.installOutput, "✓ mpv-manager removed from system PATH successfully!") m.installOutput = append(m.installOutput, "") m.installOutput = append(m.installOutput, "Press Enter to return to main menu or Esc to quit") m.updateOutputViewport() } m.RefreshMainMenu() } else { // Handle successful install switch m.installOperationType { case OpTypeInstall: log.Separator("Installation Summary") log.Info(fmt.Sprintf("Installation completed successfully: %s", getClientName(m.selectedMethodID, m.selectedMethod))) log.Info(fmt.Sprintf("App saved to config: %s", m.selectedMethodID)) m.saveInstalledApp() m.RefreshMainMenu() case OpTypeUpdate: log.Separator("Update Summary") log.Info(fmt.Sprintf("Update completed successfully: %s", getClientName(m.selectedMethodID, m.selectedMethod))) m.updateInstalledAppVersion() m.RefreshMainMenu() case OpTypeConfig: log.Separator("Config Operation Summary") log.Info("Configuration operation completed successfully") m.RefreshMainMenu() default: log.Warn("Unknown operation type completed") m.RefreshMainMenu() } } } else { // ✅ HANDLE ERRORS PROPERLY m.installSuccess = false if m.selectedUninstall != nil { // ❌ FAILED UNINSTALL log.Separator("Uninstallation Error") log.Error(fmt.Sprintf("Uninstallation failed: %v", msg.err)) log.Info("App NOT removed from installed_apps config (uninstall did not complete)") // Show error message to user m.installOutput = append(m.installOutput, "") m.installOutput = append(m.installOutput, "✗ Uninstallation failed!") m.installOutput = append(m.installOutput, fmt.Sprintf("Error: %v", msg.err)) m.installOutput = append(m.installOutput, "") m.installOutput = append(m.installOutput, "Press Enter to return to main menu or Esc to quit") m.updateOutputViewport() // Don't remove from config - app wasn't actually uninstalled // App will stay in menu for user to try again m.RefreshMainMenu() } else { // Handle failed install/update log.Error(fmt.Sprintf("Installation failed: %s", msg.err.Error())) // Show error message to user m.installOutput = append(m.installOutput, "") m.installOutput = append(m.installOutput, "✗ Installation failed!") m.installOutput = append(m.installOutput, fmt.Sprintf("Error: %v", msg.err)) m.installOutput = append(m.installOutput, "") m.installOutput = append(m.installOutput, "Press Enter to return to main menu or Esc to quit") m.updateOutputViewport() } } // Reset operation type m.installOperationType = OpTypeNone return m, nil case tickMsg: case tea.MouseMsg: if m.state == StateInstalling { switch msg.Type { case tea.MouseWheelDown: m.outputViewport.LineDown(1) m.autoScroll = false case tea.MouseWheelUp: m.outputViewport.LineUp(1) m.autoScroll = false } } } if (m.state == StateMainMenu || m.state == StateInstallMenu || m.state == StateUninstall) && !m.installing { if m.state == StateMainMenu { m.menuList, cmd = m.menuList.Update(msg) } else if m.state == StateInstallMenu { m.installMenuList, cmd = m.installMenuList.Update(msg) } else if m.state == StateUninstall { m.uninstallList, cmd = m.uninstallList.Update(msg) } } // Delegate language state handling switch m.state { case StateLanguagePreferences: m, cmd = m.handleLangPreferenceUpdate(msg) return m, cmd case StateLanguageMajorSelect: m, cmd = m.handleMajorLangUpdate(msg) return m, cmd case StateLanguageRegionSelect: m, cmd = m.handleRegionLangUpdate(msg) return m, cmd case StateLanguageSelectedList: m, cmd = m.handleSelectedLangUpdate(msg) return m, cmd case StateLanguageConfirm: switch msg := msg.(type) { case tea.KeyMsg: switch msg.Type { case tea.KeyEnter: m.state = StateMainMenu return m, nil } } return m, nil case StateConfigOptions: m, cmd = m.handleConfigOptionsUpdate(msg) return m, cmd case StateHWAOptions: m, cmd = m.handleHWAOptionsUpdate(msg) return m, cmd case StateScreenshotOptions: m, cmd = m.handleScreenshotSettingsUpdate(msg) return m, cmd // Screenshot value selection states (dropdown lists) case StateScreenshotFormatEdit: fallthrough case StateScreenshotTagColorspaceEdit: fallthrough case StateScreenshotHighBitDepthEdit: fallthrough case StateScreenshotJpegQualityEdit: fallthrough case StateScreenshotPngCompressionEdit: fallthrough case StateScreenshotWebpLosslessEdit: fallthrough case StateScreenshotWebpQualityEdit: fallthrough case StateScreenshotJxlDistanceEdit: fallthrough case StateScreenshotAvifEncoderEdit: fallthrough case StateScreenshotAvifPixfmtEdit: m, cmd = m.handleScreenshotValueSelection(msg) return m, cmd // Screenshot text input states case StateScreenshotTemplateEdit: fallthrough case StateScreenshotDirectoryEdit: fallthrough case StateScreenshotAvifOptsEdit: m, cmd = m.handleScreenshotTextInput(msg) return m, cmd case StateSavePositionOnQuitEdit: m, cmd = m.handleSavePositionOnQuitUpdate(msg) return m, cmd case StateFileAssociationsMenu: m, cmd = m.handleFileAssociationsMenuUpdate(msg) return m, cmd case StateWebUIMenu: switch msg := msg.(type) { case tea.KeyMsg: switch msg.Type { case tea.KeyEnter: if !m.installing { selected := m.webUIList.SelectedItem() if selected != nil { item := selected.(menuItem) m.selectedMethodID = item.id m.statusMessage = item.title switch m.selectedMethodID { case "create-webui-shortcut": return m, m.runCreateWebUIShortcutCmd() } } } case tea.KeyEsc: m.state = StateMainMenu m.statusMessage = "Main Menu" } } m.webUIList, cmd = m.webUIList.Update(msg) return m, cmd case StateHotkeysCategory: m, cmd = m.handleHotkeysCategoryUpdate(msg) return m, cmd case StateHotkeys: m, cmd = m.handleHotkeysUpdate(msg) return m, cmd case StateHotkeysPreset: m, cmd = m.handleHotkeysPresetUpdate(msg) return m, cmd case StateUISelect: m, cmd = m.handleUISelectUpdate(msg) return m, cmd case StateInstalling: if m.installComplete { isUpdate := strings.HasPrefix(m.currentCommand, "Updating") switch msg := msg.(type) { case tea.KeyMsg: switch msg.Type { case tea.KeyEnter: if isUpdate && m.installError == nil { return m, tea.Quit } else { m.resetToMainMenu() return m, nil } case tea.KeyEsc: m.resetToMainMenu() return m, nil } } } return m, nil case StateUpdates: if m.updateList.SettingFilter() || m.updateList.IsFiltered() { switch msg := msg.(type) { case tea.KeyMsg: if msg.Type == tea.KeyEsc { m.updateList.ResetFilter() return m, nil } } } var cmd tea.Cmd m.updateList, cmd = m.updateList.Update(msg) return m, cmd case StateUpdateList: if m.updateList.SettingFilter() || m.updateList.IsFiltered() { switch msg := msg.(type) { case tea.KeyMsg: if msg.Type == tea.KeyEsc { m.updateList.ResetFilter() return m, nil } } } var cmd tea.Cmd m.updateList, cmd = m.updateList.Update(msg) return m, cmd case StateRestoreConfig: if m.restoreConfigList.SettingFilter() || m.restoreConfigList.IsFiltered() { switch msg := msg.(type) { case tea.KeyMsg: if msg.Type == tea.KeyEsc { m.restoreConfigList.ResetFilter() return m, nil } } } var cmd tea.Cmd m.restoreConfigList, cmd = m.restoreConfigList.Update(msg) return m, cmd } return m, cmd } func (m *Model) saveInstalledApp() { if m.selectedMethodID == "install-to-path" { return } appName := getClientName(m.selectedMethodID, m.selectedMethod) installType := m.getInstallType() installPath := m.getInstallPath() // Get version from appropriate source // Note: Don't save version for package-manager and flatpak // since these can be updated outside of our app appVersion := "" if installType != "package-manager" && installType != "flatpak" { appVersion = m.getInstalledAppVersion() } app := config.InstalledApp{ AppName: appName, AppType: installType, InstallMethod: m.selectedMethodID, InstallPath: installPath, InstalledDate: time.Now().Format("2006-01-02 15:04:05"), AppVersion: appVersion, Managed: true, // Installed and managed by MPV Manager } if err := config.AddInstalledApp(app); err != nil { fmt.Printf("Error saving installed app: %v\n", err) } } func (m *Model) getInstallType() string { if meta, ok := MethodIDMetadata[m.selectedMethodID]; ok { return meta.Type } return "" } func (m *Model) getInstalledAppVersion() string { if m.installer != nil { if meta, ok := MethodIDMetadata[m.selectedMethodID]; ok && meta.Version != "" { switch meta.Version { case "mpv": return m.installer.ReleaseInfo.MpvVersion case "mpc-qt": return m.installer.ReleaseInfo.MPCQT.AppVersion case "iina": return m.installer.ReleaseInfo.IINA.AppVersion } } } return "" } func (m *Model) getInstallPath() string { if meta, ok := MethodIDMetadata[m.selectedMethodID]; ok { if meta.Type == "exe-binary" || meta.Type == "exe-installer" { return config.GetInstallPath() } } return "" } func (m *Model) updateInstalledAppVersion() { appVersion := m.getInstalledAppVersion() if appVersion == "" { return } cfg, err := config.Load() if err != nil { log.Warn(fmt.Sprintf("Failed to load config: %v", err)) return } found := false for i, app := range cfg.InstalledApps { if app.InstallMethod == m.selectedMethodID { cfg.InstalledApps[i].AppVersion = appVersion found = true log.Separator("Update Summary") log.Info(fmt.Sprintf("Version updated successfully: %s", app.AppName)) log.Info(fmt.Sprintf("New version: %s", appVersion)) break } } if found { if err := config.Save(); err != nil { log.Warn(fmt.Sprintf("Failed to save config: %v", err)) } } else { log.Warn(fmt.Sprintf("App not found for update: %s", m.selectedMethodID)) } } func (m *Model) resetToMainMenu() { m.state = StateMainMenu m.selectedMethod = "" m.selectedMethodID = "" m.installing = false m.installComplete = false m.installOutput = []string{} m.currentCommand = "" m.selectedUninstall = nil m.selectedRestoreBackup = nil m.postInstallMessage = "" m.pendingMethodID = "" m.pendingAppName = "" m.selectedUIType = "" } func (m *Model) handleConfigOptionsUpdate(msg tea.Msg) (*Model, tea.Cmd) { var cmd tea.Cmd m.configOptionsList, cmd = m.configOptionsList.Update(msg) switch msg := msg.(type) { case tea.KeyMsg: switch msg.Type { case tea.KeyEsc: m.state = StateMainMenu return m, nil case tea.KeyEnter: selected := m.configOptionsList.SelectedItem() if selected != nil { item := selected.(menuItem) switch item.id { case "reset-config": return m, m.runResetConfigCmd() case "restore-config": if err := m.loadAndSetupBackupList(); err != nil { m.errorMessage = "Failed to load config backups: " + err.Error() m.state = StateError return m, nil } m.state = StateRestoreConfig return m, nil case "change-hwdec": m.setupHWAOptionsList() m.state = StateHWAOptions return m, nil case "save-position-on-quit": m.setupSavePositionOnQuitList() m.state = StateSavePositionOnQuitEdit return m, nil case "screenshot-settings": m.setupScreenshotOptionsList() m.state = StateScreenshotOptions return m, nil case "file-associations": m.setupFileAssociationsList() m.state = StateFileAssociationsMenu return m, nil case "change-mpv-ui": // Find the first MPV app to change UI for apps := config.GetInstalledApps() mpvMethods := []string{ "mpv-flatpak", "mpv-package", "mpv-binary", "mpv-binary-v3", "mpv-app", "mpv-brew", } for _, app := range apps { for _, method := range mpvMethods { if app.InstallMethod == method { m.pendingMethodID = method m.pendingAppName = app.AppName m.setupUISelectList() m.state = StateUISelect return m, nil } } } return m, nil } } } } return m, cmd } func (m *Model) handleHWAOptionsUpdate(msg tea.Msg) (*Model, tea.Cmd) { var cmd tea.Cmd m.hwaOptionsList, cmd = m.hwaOptionsList.Update(msg) switch msg := msg.(type) { case tea.KeyMsg: switch msg.Type { case tea.KeyEsc: m.setupConfigOptionsList() m.state = StateConfigOptions return m, nil case tea.KeyEnter: selected := m.hwaOptionsList.SelectedItem() if selected != nil { item := selected.(hwaOptionItem) m.selectedHWAOption = item.id return m, m.runApplyHWACmd() } } } return m, cmd } func (m *Model) handleSavePositionOnQuitUpdate(msg tea.Msg) (*Model, tea.Cmd) { var cmd tea.Cmd m.screenshotValueList, cmd = m.screenshotValueList.Update(msg) switch msg := msg.(type) { case tea.KeyMsg: switch msg.Type { case tea.KeyEsc: m.setupConfigOptionsList() m.state = StateConfigOptions return m, nil case tea.KeyEnter: selected := m.screenshotValueList.SelectedItem() if selected != nil { item := selected.(screenshotValueItem) // Apply the setting if err := config.SetConfigValue(constants.ConfigKeySavePositionOnQuit, []string{item.id}, false); err != nil { m.errorMessage = "Failed to apply save-position-on-quit setting: " + err.Error() m.state = StateError return m, nil } m.statusMessage = "Save Position on Quit set to: " + item.id m.setupConfigOptionsList() m.state = StateConfigOptions return m, nil } } } return m, cmd } func (m *Model) handleHotkeysCategoryUpdate(msg tea.Msg) (*Model, tea.Cmd) { var cmd tea.Cmd m.hotkeysCategoryList, cmd = m.hotkeysCategoryList.Update(msg) switch msg := msg.(type) { case tea.KeyMsg: switch msg.Type { case tea.KeyEsc: m.setupHotkeysPresetList() m.state = StateHotkeysPreset m.statusMessage = "Keybinding Presets" return m, nil case tea.KeyEnter: selected := m.hotkeysCategoryList.SelectedItem() if selected != nil { item := selected.(hotkeyCategoryItem) m.setupHotkeysList(item.id) m.statusMessage = item.title m.state = StateHotkeys return m, nil } } } return m, cmd } func (m *Model) handleHotkeysUpdate(msg tea.Msg) (*Model, tea.Cmd) { var cmd tea.Cmd m.hotkeysList, cmd = m.hotkeysList.Update(msg) switch msg := msg.(type) { case tea.KeyMsg: switch msg.Type { case tea.KeyEsc: m.setupHotkeysCategoryList() m.state = StateHotkeysCategory m.statusMessage = "Keyboard Shortcuts" return m, nil } } return m, cmd } func (m *Model) handleHotkeysPresetUpdate(msg tea.Msg) (*Model, tea.Cmd) { var cmd tea.Cmd m.hotkeysPresetList, cmd = m.hotkeysPresetList.Update(msg) switch msg := msg.(type) { case tea.KeyMsg: switch msg.Type { case tea.KeyEsc: m.state = StateMainMenu m.statusMessage = "Main Menu" return m, nil case tea.KeyEnter: selected := m.hotkeysPresetList.SelectedItem() if selected != nil { item := selected.(hotkeyPresetItem) if item.id == "view-all" { // Go to category list m.setupHotkeysCategoryList() m.state = StateHotkeysCategory m.statusMessage = "Keyboard Shortcuts" return m, nil } // Apply the selected preset return m, m.runApplyPresetCmd(item.id) } } case presetApplyMsg: if msg.err != nil { m.presetStatusMessage = fmt.Sprintf("✗ Failed to apply preset: %v", msg.err) } else { m.presetStatusMessage = fmt.Sprintf("✓ Applied preset: %s", msg.preset) } return m, nil } return m, cmd } func (m *Model) handleFileAssociationsMenuUpdate(msg tea.Msg) (*Model, tea.Cmd) { var cmd tea.Cmd m.fileAssociationsList, cmd = m.fileAssociationsList.Update(msg) switch msg := msg.(type) { case tea.KeyMsg: switch msg.Type { case tea.KeyEsc: m.setupConfigOptionsList() m.state = StateConfigOptions return m, nil case tea.KeyEnter: selected := m.fileAssociationsList.SelectedItem() if selected != nil { item := selected.(menuItem) switch item.id { case "install-associations": return m, m.runInstallAssociationsCmd() case "remove-associations": return m, m.runRemoveAssociationsCmd() } } } } return m, cmd } func (m *Model) handleScreenshotSettingsUpdate(msg tea.Msg) (*Model, tea.Cmd) { var cmd tea.Cmd switch msg := msg.(type) { case tea.KeyMsg: switch msg.Type { case tea.KeyEnter: selected := m.screenshotOptionsList.SelectedItem() if selected != nil { if option, ok := selected.(screenshotOptionItem); ok { m.selectedScreenshotOption = option.id // Set up editing interface based on option type switch option.id { case "screenshot-format": m.setupScreenshotFormatList() m.state = StateScreenshotFormatEdit case "screenshot-tag-colorspace": m.setupScreenshotYesNoList("tag-colorspace", "Tag Colorspace in Filename") m.state = StateScreenshotTagColorspaceEdit case "screenshot-high-bit-depth": m.setupScreenshotYesNoList("high-bit-depth", "High Bit Depth") m.state = StateScreenshotHighBitDepthEdit case "screenshot-template": // Initialize textinput with current value currentVal := getCurrentScreenshotValue("template") if currentVal == "Not set" { currentVal = "mpv-shot%n" } ti := textinput.New() ti.Placeholder = "Enter filename template..." ti.SetValue(currentVal) ti.CharLimit = 156 ti.Width = 50 ti.Focus() m.screenshotTextInput = ti m.state = StateScreenshotTemplateEdit case "screenshot-directory": // Initialize textinput with current value currentVal := getCurrentScreenshotValue("directory") if currentVal == "Not set" { currentVal = "" } ti := textinput.New() ti.Placeholder = "Enter directory path..." ti.SetValue(currentVal) ti.CharLimit = 256 ti.Width = 50 ti.Focus() m.screenshotTextInput = ti m.state = StateScreenshotDirectoryEdit case "screenshot-jpeg-quality": values := make([]string, 101) for i := 0; i <= 100; i++ { values[i] = fmt.Sprintf("%d", i) } m.setupScreenshotNumericList("jpeg-quality", "JPEG Quality (0-100)", values) m.state = StateScreenshotJpegQualityEdit case "screenshot-png-compression": values := make([]string, 10) for i := 0; i <= 9; i++ { values[i] = fmt.Sprintf("%d", i) } m.setupScreenshotNumericList("png-compression", "PNG Compression (0-9)", values) m.state = StateScreenshotPngCompressionEdit case "screenshot-webp-lossless": m.setupScreenshotYesNoList("webp-lossless", "WebP Lossless") m.state = StateScreenshotWebpLosslessEdit case "screenshot-webp-quality": values := make([]string, 101) for i := 0; i <= 100; i++ { values[i] = fmt.Sprintf("%d", i) } m.setupScreenshotNumericList("webp-quality", "WebP Quality (0-100)", values) m.state = StateScreenshotWebpQualityEdit case "screenshot-jxl-distance": values := []string{"0.0", "0.1", "0.2", "0.3", "0.4", "0.5", "1.0", "1.5", "2.0", "2.5", "3.0", "4.0", "5.0", "10.0", "15.0", "20.0", "25.0"} m.setupScreenshotNumericList("jxl-distance", "JPEG XL Distance", values) m.state = StateScreenshotJxlDistanceEdit case "screenshot-avif-encoder": m.setupScreenshotEncoderList() m.state = StateScreenshotAvifEncoderEdit case "screenshot-avif-pixfmt": m.setupScreenshotPixelFormatList() m.state = StateScreenshotAvifPixfmtEdit case "screenshot-avif-opts": // Initialize textinput with current value currentVal := getCurrentScreenshotValue("avif-opts") if currentVal == "Not set" { currentVal = "usage=allintra,crf=0,cpu-used=8" } ti := textinput.New() ti.Placeholder = "Enter encoder options..." ti.SetValue(currentVal) ti.CharLimit = 256 ti.Width = 50 ti.Focus() m.screenshotTextInput = ti m.state = StateScreenshotAvifOptsEdit } return m, nil } } case tea.KeyEsc: m.setupConfigOptionsList() m.state = StateConfigOptions return m, nil } } m.screenshotOptionsList, cmd = m.screenshotOptionsList.Update(msg) return m, cmd } // Generic handler for screenshot value selection (dropdown lists) func (m *Model) handleScreenshotValueSelection(msg tea.Msg) (*Model, tea.Cmd) { var cmd tea.Cmd m.screenshotValueList, cmd = m.screenshotValueList.Update(msg) switch msg := msg.(type) { case tea.KeyMsg: switch msg.Type { case tea.KeyEnter: selected := m.screenshotValueList.SelectedItem() if selected != nil { if val, ok := selected.(screenshotValueItem); ok { // Apply selected value key := getScreenshotConfigKey(m.selectedScreenshotOption) if err := config.SetConfigValue(key, []string{val.id}, false); err != nil { m.errorMessage = fmt.Sprintf("Failed to set %s: %v", key, err) m.state = StateError return m, nil } // Return to screenshot options list and refresh m.setupScreenshotOptionsList() m.state = StateScreenshotOptions } } case tea.KeyEsc: m.setupScreenshotOptionsList() m.state = StateScreenshotOptions return m, nil } } return m, cmd } // Handler for screenshot text input fields func (m *Model) handleScreenshotTextInput(msg tea.Msg) (*Model, tea.Cmd) { var cmd tea.Cmd var tiCmd tea.Cmd m.screenshotTextInput, tiCmd = m.screenshotTextInput.Update(msg) switch msg := msg.(type) { case tea.KeyMsg: switch msg.Type { case tea.KeyEnter: // Apply the entered value value := m.screenshotTextInput.Value() key := getScreenshotConfigKey(m.selectedScreenshotOption) if err := config.SetConfigValue(key, []string{value}, false); err != nil { m.errorMessage = fmt.Sprintf("Failed to set %s: %v", key, err) m.state = StateError return m, nil } // Return to screenshot options list and refresh m.setupScreenshotOptionsList() m.state = StateScreenshotOptions case tea.KeyEsc: m.setupScreenshotOptionsList() m.state = StateScreenshotOptions return m, nil } } return m, tea.Batch(cmd, tiCmd) } func (m *Model) runResetConfigCmd() tea.Cmd { m.statusMessage = "Resetting MPV config to recommended..." m.installing = true m.state = StateInstalling m.installOperationType = OpTypeConfig outputChan := make(chan string) installDone := make(chan error) return func() tea.Msg { go func() { cr := installer.NewCommandRunner(outputChan, nil) err := m.installer.InstallMPVConfigWithOutput(cr) installDone <- err close(outputChan) }() return saveChannelsMsg{ outputChan: outputChan, installDone: installDone, } } } func (m *Model) runApplyHWACmd() tea.Cmd { m.statusMessage = "Applying hardware acceleration setting..." m.installing = true m.state = StateInstalling m.installOperationType = OpTypeConfig outputChan := make(chan string) installDone := make(chan error) return func() tea.Msg { go func() { outputChan <- fmt.Sprintf("Setting hwdec=%s", m.selectedHWAOption) if err := m.applyHWAOption(m.selectedHWAOption); err != nil { outputChan <- "Failed to apply hardware acceleration: " + err.Error() installDone <- err close(outputChan) return } outputChan <- fmt.Sprintf("Hardware acceleration set to: %s", m.selectedHWAOption) outputChan <- "✓ Configuration updated successfully" installDone <- nil close(outputChan) }() return saveChannelsMsg{ outputChan: outputChan, installDone: installDone, } } } func (m *Model) runRestoreConfigCmd() tea.Cmd { outputChan := make(chan string) installDone := make(chan error) return func() tea.Msg { go func() { cr := installer.NewCommandRunner(outputChan, nil) var err error if m.selectedRestoreBackup != nil { err = m.installer.RestoreConfigWithOutput(cr, m.selectedRestoreBackup.BackupPath) } installDone <- err close(outputChan) }() return saveChannelsMsg{ outputChan: outputChan, installDone: installDone, } } } func (m *Model) runInstallAssociationsCmd() tea.Cmd { m.statusMessage = "Installing File Associations" m.installing = true m.state = StateInstalling m.installOperationType = OpTypeConfig outputChan := make(chan string) installDone := make(chan error) return func() tea.Msg { go func() { handler := installer.NewInstallationHandler(m.platformInfo, m.installer) cr := installer.NewCommandRunner(outputChan, nil) err := handler.SetupFileAssociationsWithOutput(cr) installDone <- err close(outputChan) }() return saveChannelsMsg{ outputChan: outputChan, installDone: installDone, } } } func (m *Model) runRemoveAssociationsCmd() tea.Cmd { m.statusMessage = "Removing File Associations" m.installing = true m.state = StateInstalling m.installOperationType = OpTypeConfig outputChan := make(chan string) installDone := make(chan error) return func() tea.Msg { go func() { handler := installer.NewInstallationHandler(m.platformInfo, m.installer) cr := installer.NewCommandRunner(outputChan, nil) err := handler.RemoveFileAssociationsWithOutput(cr) installDone <- err close(outputChan) }() return saveChannelsMsg{ outputChan: outputChan, installDone: installDone, } } } func (m *Model) runCreateWebUIShortcutCmd() tea.Cmd { m.statusMessage = "Creating Web UI Shortcut" m.installing = true m.state = StateInstalling m.installOperationType = OpTypeConfig outputChan := make(chan string) installDone := make(chan error) return func() tea.Msg { go func() { handler := installer.NewInstallationHandler(m.platformInfo, m.installer) cr := installer.NewCommandRunner(outputChan, nil) err := handler.CreateWebUIShortcutWithOutput(cr) installDone <- err close(outputChan) }() return saveChannelsMsg{ outputChan: outputChan, installDone: installDone, } } } func (m *Model) loadAndSetupBackupList() error { backupDir := config.GetMPVConfigPath() + "/conf_backups" backupDir = strings.ReplaceAll(backupDir, "~", "") files, err := os.ReadDir(backupDir) if err != nil { return err } var items []list.Item for _, file := range files { fileName := file.Name() if strings.HasSuffix(fileName, ".conf") { backupPath := filepath.Join(backupDir, fileName) info, err := os.Stat(backupPath) if err != nil { continue } backupName := strings.TrimSuffix(fileName, ".conf") backup := version.ConfigBackupItem{ BackupName: backupName, BackupPath: backupPath, BackupDate: info.ModTime().Format("2006-01-02 15:04"), } items = append(items, configBackupItem{backup: &backup}) } } m.restoreConfigList = m.createStyledList("Select Backup to Restore", items) return nil } // handleUISelectUpdate handles the UI selection state func (m *Model) handleUISelectUpdate(msg tea.Msg) (*Model, tea.Cmd) { var cmd tea.Cmd switch msg := msg.(type) { case tea.KeyMsg: switch msg.Type { case tea.KeyEnter: selected := m.uiSelectList.SelectedItem() if selected != nil { uiItem := selected.(uiOptionItem) m.selectedUIType = uiItem.id m.state = StateInstalling m.installProgress = 0 m.installing = true m.installComplete = false m.installOperationType = OpTypeInstall // Set the selected method for proper tracking m.selectedMethodID = m.pendingMethodID m.selectedMethod = getAppNameForMethod(m.pendingMethodID) installDir := m.getInstallPath() return m, executeInstallCmdWithUI(m.pendingMethodID, installDir, m.selectedUIType, m.platformInfo, m.installer) } case tea.KeyEsc: // Go back to install menu m.state = StateInstallMenu m.pendingMethodID = "" m.pendingAppName = "" return m, nil } } m.uiSelectList, cmd = m.uiSelectList.Update(msg) return m, cmd }