package tui import ( "context" "fmt" "strings" "time" tea "github.com/charmbracelet/bubbletea" "dbbackup/internal/config" "dbbackup/internal/database" "dbbackup/internal/logger" ) // DatabaseSelectorModel for selecting a database type DatabaseSelectorModel struct { config *config.Config logger logger.Logger parent tea.Model databases []string cursor int selected string loading bool err error title string message string backupType string // "single" or "sample" } func NewDatabaseSelector(cfg *config.Config, log logger.Logger, parent tea.Model, title string, backupType string) DatabaseSelectorModel { return DatabaseSelectorModel{ config: cfg, logger: log, parent: parent, databases: []string{"Loading databases..."}, title: title, loading: true, backupType: backupType, } } func (m DatabaseSelectorModel) Init() tea.Cmd { return fetchDatabases(m.config, m.logger) } type databaseListMsg struct { databases []string err error } func fetchDatabases(cfg *config.Config, log logger.Logger) tea.Cmd { return func() tea.Msg { ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) defer cancel() dbClient, err := database.New(cfg, log) if err != nil { return databaseListMsg{databases: nil, err: fmt.Errorf("failed to create database client: %w", err)} } defer dbClient.Close() if err := dbClient.Connect(ctx); err != nil { return databaseListMsg{databases: nil, err: fmt.Errorf("connection failed: %w", err)} } databases, err := dbClient.ListDatabases(ctx) if err != nil { return databaseListMsg{databases: nil, err: fmt.Errorf("failed to list databases: %w", err)} } return databaseListMsg{databases: databases, err: nil} } } func (m DatabaseSelectorModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case databaseListMsg: m.loading = false if msg.err != nil { m.err = msg.err m.databases = []string{"Error loading databases"} } else { m.databases = msg.databases } return m, nil case tea.KeyMsg: switch msg.String() { case "ctrl+c", "q", "esc": return m.parent, nil case "up", "k": if m.cursor > 0 { m.cursor-- } case "down", "j": if m.cursor < len(m.databases)-1 { m.cursor++ } case "enter": if !m.loading && m.err == nil && len(m.databases) > 0 { m.selected = m.databases[m.cursor] // If sample backup, ask for ratio first if m.backupType == "sample" { inputModel := NewInputModel(m.config, m.logger, m, "šŸ“Š Sample Ratio", "Enter sample ratio (1-100):", "10", ValidateInt(1, 100)) return inputModel, nil } // For single backup, go directly to execution executor := NewBackupExecution(m.config, m.logger, m.parent, m.backupType, m.selected, 0) return executor, executor.Init() } } } return m, nil } func (m DatabaseSelectorModel) View() string { var s strings.Builder header := titleStyle.Render(m.title) s.WriteString(fmt.Sprintf("\n%s\n\n", header)) if m.loading { s.WriteString("ā³ Loading databases...\n") return s.String() } if m.err != nil { s.WriteString(fmt.Sprintf("āŒ Error: %v\n", m.err)) s.WriteString("\nPress ESC to go back\n") return s.String() } s.WriteString("Select a database:\n\n") for i, db := range m.databases { cursor := " " if m.cursor == i { cursor = ">" s.WriteString(selectedStyle.Render(fmt.Sprintf("%s %s", cursor, db))) } else { s.WriteString(fmt.Sprintf("%s %s", cursor, db)) } s.WriteString("\n") } if m.message != "" { s.WriteString(fmt.Sprintf("\n%s\n", m.message)) } s.WriteString("\nāŒØļø ↑/↓: Navigate • Enter: Select • ESC: Back • q: Quit\n") return s.String() } func (m DatabaseSelectorModel) GetSelected() string { return m.selected }