package tui import ( "fmt" "path/filepath" "strconv" "strings" tea "github.com/charmbracelet/bubbletea" "dbbackup/internal/config" "dbbackup/internal/logger" ) // SettingsModel represents the settings configuration state type SettingsModel struct { config *config.Config logger logger.Logger cursor int editing bool editingField string editingValue string settings []SettingItem quitting bool message string parent tea.Model } // SettingItem represents a configurable setting type SettingItem struct { Key string DisplayName string Value func(*config.Config) string Update func(*config.Config, string) error Type string // "string", "int", "bool", "path" Description string } // Initialize settings model func NewSettingsModel(cfg *config.Config, log logger.Logger, parent tea.Model) SettingsModel { settings := []SettingItem{ { Key: "backup_dir", DisplayName: "Backup Directory", Value: func(c *config.Config) string { return c.BackupDir }, Update: func(c *config.Config, v string) error { if v == "" { return fmt.Errorf("backup directory cannot be empty") } c.BackupDir = filepath.Clean(v) return nil }, Type: "path", Description: "Directory where backup files will be stored", }, { Key: "compression_level", DisplayName: "Compression Level", Value: func(c *config.Config) string { return fmt.Sprintf("%d", c.CompressionLevel) }, Update: func(c *config.Config, v string) error { val, err := strconv.Atoi(v) if err != nil { return fmt.Errorf("compression level must be a number") } if val < 0 || val > 9 { return fmt.Errorf("compression level must be between 0-9") } c.CompressionLevel = val return nil }, Type: "int", Description: "Compression level (0=fastest, 9=smallest)", }, { Key: "jobs", DisplayName: "Parallel Jobs", Value: func(c *config.Config) string { return fmt.Sprintf("%d", c.Jobs) }, Update: func(c *config.Config, v string) error { val, err := strconv.Atoi(v) if err != nil { return fmt.Errorf("jobs must be a number") } if val < 1 { return fmt.Errorf("jobs must be at least 1") } c.Jobs = val return nil }, Type: "int", Description: "Number of parallel jobs for backup operations", }, { Key: "dump_jobs", DisplayName: "Dump Jobs", Value: func(c *config.Config) string { return fmt.Sprintf("%d", c.DumpJobs) }, Update: func(c *config.Config, v string) error { val, err := strconv.Atoi(v) if err != nil { return fmt.Errorf("dump jobs must be a number") } if val < 1 { return fmt.Errorf("dump jobs must be at least 1") } c.DumpJobs = val return nil }, Type: "int", Description: "Number of parallel jobs for database dumps", }, { Key: "host", DisplayName: "Database Host", Value: func(c *config.Config) string { return c.Host }, Update: func(c *config.Config, v string) error { if v == "" { return fmt.Errorf("host cannot be empty") } c.Host = v return nil }, Type: "string", Description: "Database server hostname or IP address", }, { Key: "port", DisplayName: "Database Port", Value: func(c *config.Config) string { return fmt.Sprintf("%d", c.Port) }, Update: func(c *config.Config, v string) error { val, err := strconv.Atoi(v) if err != nil { return fmt.Errorf("port must be a number") } if val < 1 || val > 65535 { return fmt.Errorf("port must be between 1-65535") } c.Port = val return nil }, Type: "int", Description: "Database server port number", }, { Key: "user", DisplayName: "Database User", Value: func(c *config.Config) string { return c.User }, Update: func(c *config.Config, v string) error { if v == "" { return fmt.Errorf("user cannot be empty") } c.User = v return nil }, Type: "string", Description: "Database username for connections", }, { Key: "database", DisplayName: "Default Database", Value: func(c *config.Config) string { return c.Database }, Update: func(c *config.Config, v string) error { c.Database = v // Can be empty for cluster operations return nil }, Type: "string", Description: "Default database name (optional)", }, { Key: "ssl_mode", DisplayName: "SSL Mode", Value: func(c *config.Config) string { return c.SSLMode }, Update: func(c *config.Config, v string) error { validModes := []string{"disable", "allow", "prefer", "require", "verify-ca", "verify-full"} for _, mode := range validModes { if v == mode { c.SSLMode = v return nil } } return fmt.Errorf("invalid SSL mode. Valid options: %s", strings.Join(validModes, ", ")) }, Type: "string", Description: "SSL connection mode (disable, allow, prefer, require, verify-ca, verify-full)", }, { Key: "auto_detect_cores", DisplayName: "Auto Detect CPU Cores", Value: func(c *config.Config) string { if c.AutoDetectCores { return "true" } else { return "false" } }, Update: func(c *config.Config, v string) error { val, err := strconv.ParseBool(v) if err != nil { return fmt.Errorf("must be true or false") } c.AutoDetectCores = val return nil }, Type: "bool", Description: "Automatically detect and optimize for CPU cores", }, } return SettingsModel{ config: cfg, logger: log, settings: settings, parent: parent, } } // Init initializes the settings model func (m SettingsModel) Init() tea.Cmd { return nil } // Update handles messages func (m SettingsModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: if m.editing { return m.handleEditingInput(msg) } switch msg.String() { case "ctrl+c", "q", "esc": m.quitting = true return m.parent, nil case "up", "k": if m.cursor > 0 { m.cursor-- } case "down", "j": if m.cursor < len(m.settings)-1 { m.cursor++ } case "enter", " ": return m.startEditing() case "r": return m.resetToDefaults() case "s": return m.saveSettings() } } return m, nil } // handleEditingInput handles input when editing a setting func (m SettingsModel) handleEditingInput(msg tea.KeyMsg) (tea.Model, tea.Cmd) { switch msg.String() { case "ctrl+c": m.quitting = true return m.parent, nil case "esc": m.editing = false m.editingField = "" m.editingValue = "" m.message = "" return m, nil case "enter": return m.saveEditedValue() case "backspace": if len(m.editingValue) > 0 { m.editingValue = m.editingValue[:len(m.editingValue)-1] } default: // Add character to editing value if len(msg.String()) == 1 { m.editingValue += msg.String() } } return m, nil } // startEditing begins editing a setting func (m SettingsModel) startEditing() (tea.Model, tea.Cmd) { if m.cursor >= len(m.settings) { return m, nil } setting := m.settings[m.cursor] m.editing = true m.editingField = setting.Key m.editingValue = setting.Value(m.config) m.message = "" return m, nil } // saveEditedValue saves the currently edited value func (m SettingsModel) saveEditedValue() (tea.Model, tea.Cmd) { if m.editingField == "" { return m, nil } // Find the setting being edited var setting *SettingItem for i := range m.settings { if m.settings[i].Key == m.editingField { setting = &m.settings[i] break } } if setting == nil { m.message = errorStyle.Render("❌ Setting not found") m.editing = false return m, nil } // Update the configuration if err := setting.Update(m.config, m.editingValue); err != nil { m.message = errorStyle.Render(fmt.Sprintf("❌ %s", err.Error())) return m, nil } m.message = successStyle.Render(fmt.Sprintf("✅ Updated %s", setting.DisplayName)) m.editing = false m.editingField = "" m.editingValue = "" return m, nil } // resetToDefaults resets configuration to default values func (m SettingsModel) resetToDefaults() (tea.Model, tea.Cmd) { newConfig := config.New() // Copy important connection details newConfig.Host = m.config.Host newConfig.Port = m.config.Port newConfig.User = m.config.User newConfig.Database = m.config.Database newConfig.DatabaseType = m.config.DatabaseType *m.config = *newConfig m.message = successStyle.Render("✅ Settings reset to defaults") return m, nil } // saveSettings validates and saves current settings func (m SettingsModel) saveSettings() (tea.Model, tea.Cmd) { if err := m.config.Validate(); err != nil { m.message = errorStyle.Render(fmt.Sprintf("❌ Validation failed: %s", err.Error())) return m, nil } // Optimize CPU settings if auto-detect is enabled if m.config.AutoDetectCores { if err := m.config.OptimizeForCPU(); err != nil { m.message = errorStyle.Render(fmt.Sprintf("❌ CPU optimization failed: %s", err.Error())) return m, nil } } m.message = successStyle.Render("✅ Settings validated and saved") return m, nil } // View renders the settings interface func (m SettingsModel) View() string { if m.quitting { return "Returning to main menu...\n" } var b strings.Builder // Header header := titleStyle.Render("⚙️ Configuration Settings") b.WriteString(fmt.Sprintf("\n%s\n\n", header)) // Settings list for i, setting := range m.settings { cursor := " " value := setting.Value(m.config) if m.cursor == i { cursor = ">" if m.editing && m.editingField == setting.Key { // Show editing interface editValue := m.editingValue if setting.Type == "bool" { editValue += " (true/false)" } line := fmt.Sprintf("%s %s: %s", cursor, setting.DisplayName, editValue) b.WriteString(selectedStyle.Render(line)) b.WriteString(" ✏️") } else { line := fmt.Sprintf("%s %s: %s", cursor, setting.DisplayName, value) b.WriteString(selectedStyle.Render(line)) } } else { line := fmt.Sprintf("%s %s: %s", cursor, setting.DisplayName, value) b.WriteString(menuStyle.Render(line)) } b.WriteString("\n") // Show description for selected item if m.cursor == i && !m.editing { desc := detailStyle.Render(fmt.Sprintf(" %s", setting.Description)) b.WriteString(desc) b.WriteString("\n") } } // Message area if m.message != "" { b.WriteString("\n") b.WriteString(m.message) b.WriteString("\n") } // Current configuration summary if !m.editing { b.WriteString("\n") b.WriteString(infoStyle.Render("📋 Current Configuration:")) b.WriteString("\n") summary := []string{ fmt.Sprintf("Database: %s@%s:%d", m.config.User, m.config.Host, m.config.Port), fmt.Sprintf("Backup Dir: %s", m.config.BackupDir), fmt.Sprintf("Compression: Level %d", m.config.CompressionLevel), fmt.Sprintf("Jobs: %d parallel, %d dump", m.config.Jobs, m.config.DumpJobs), } for _, line := range summary { b.WriteString(detailStyle.Render(fmt.Sprintf(" %s", line))) b.WriteString("\n") } } // Footer with instructions var footer string if m.editing { footer = infoStyle.Render("\n⌨️ Type new value • Enter to save • Esc to cancel") } else { footer = infoStyle.Render("\n⌨️ ↑/↓ navigate • Enter to edit • 's' save • 'r' reset • 'q' back to menu") } b.WriteString(footer) return b.String() } // RunSettingsMenu starts the settings configuration interface func RunSettingsMenu(cfg *config.Config, log logger.Logger, parent tea.Model) error { m := NewSettingsModel(cfg, log, parent) p := tea.NewProgram(m, tea.WithAltScreen()) if _, err := p.Run(); err != nil { return fmt.Errorf("error running settings menu: %w", err) } return nil }