- [CHECK] for diagnosis, previews, validations - [STATS] for status, history, metrics views - [SELECT] for selection/browsing screens - [EXEC] for execution screens (backup/restore) - [CONFIG] for settings/configuration Fixed 8 files with inconsistent prefixes: - diagnose_view.go: [SEARCH] → [CHECK] - settings.go: [CFG] → [CONFIG] - menu.go: [DB] → clean title - history.go: [HISTORY] → [STATS] - backup_manager.go: [DB] → [SELECT] - archive_browser.go: [PKG]/[SEARCH] → [SELECT] - restore_preview.go: added [CHECK] - restore_exec.go: [RESTORE] → [EXEC]
832 lines
22 KiB
Go
Executable File
832 lines
22 KiB
Go
Executable File
package tui
|
|
|
|
import (
|
|
"fmt"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
|
|
tea "github.com/charmbracelet/bubbletea"
|
|
"github.com/charmbracelet/lipgloss"
|
|
|
|
"dbbackup/internal/config"
|
|
"dbbackup/internal/logger"
|
|
)
|
|
|
|
var (
|
|
headerStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("250")).Padding(1, 2)
|
|
inputStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("15"))
|
|
buttonStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("15")).Background(lipgloss.Color("240")).Padding(0, 2)
|
|
selectedStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("15")).Background(lipgloss.Color("240")).Bold(true)
|
|
detailStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("244")).Italic(true)
|
|
)
|
|
|
|
// 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
|
|
dirBrowser *DirectoryBrowser
|
|
browsingDir bool
|
|
}
|
|
|
|
// 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: "database_type",
|
|
DisplayName: "Database Type",
|
|
Value: func(c *config.Config) string { return c.DatabaseType },
|
|
Update: func(c *config.Config, v string) error {
|
|
return c.SetDatabaseType(v)
|
|
},
|
|
Type: "selector",
|
|
Description: "Target database engine (press Enter to cycle: PostgreSQL → MySQL → MariaDB)",
|
|
},
|
|
{
|
|
Key: "cpu_workload",
|
|
DisplayName: "CPU Workload Type",
|
|
Value: func(c *config.Config) string { return c.CPUWorkloadType },
|
|
Update: func(c *config.Config, v string) error {
|
|
workloads := []string{"balanced", "cpu-intensive", "io-intensive"}
|
|
currentIdx := 0
|
|
for i, w := range workloads {
|
|
if c.CPUWorkloadType == w {
|
|
currentIdx = i
|
|
break
|
|
}
|
|
}
|
|
nextIdx := (currentIdx + 1) % len(workloads)
|
|
c.CPUWorkloadType = workloads[nextIdx]
|
|
|
|
// Recalculate Jobs and DumpJobs based on workload type
|
|
if c.CPUInfo != nil && c.AutoDetectCores {
|
|
switch c.CPUWorkloadType {
|
|
case "cpu-intensive":
|
|
c.Jobs = c.CPUInfo.PhysicalCores * 2
|
|
c.DumpJobs = c.CPUInfo.PhysicalCores
|
|
case "io-intensive":
|
|
c.Jobs = c.CPUInfo.PhysicalCores / 2
|
|
if c.Jobs < 1 {
|
|
c.Jobs = 1
|
|
}
|
|
c.DumpJobs = 2
|
|
default: // balanced
|
|
c.Jobs = c.CPUInfo.PhysicalCores
|
|
c.DumpJobs = c.CPUInfo.PhysicalCores / 2
|
|
if c.DumpJobs < 2 {
|
|
c.DumpJobs = 2
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
Type: "selector",
|
|
Description: "CPU workload profile (press Enter to cycle: Balanced → CPU-Intensive → I/O-Intensive)",
|
|
},
|
|
{
|
|
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: "work_dir",
|
|
DisplayName: "Work Directory",
|
|
Value: func(c *config.Config) string {
|
|
if c.WorkDir == "" {
|
|
return "(system temp)"
|
|
}
|
|
return c.WorkDir
|
|
},
|
|
Update: func(c *config.Config, v string) error {
|
|
if v == "" || v == "(system temp)" {
|
|
c.WorkDir = ""
|
|
return nil
|
|
}
|
|
c.WorkDir = filepath.Clean(v)
|
|
return nil
|
|
},
|
|
Type: "path",
|
|
Description: "Working directory for large operations (extraction, diagnosis). Use when /tmp is too small.",
|
|
},
|
|
{
|
|
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",
|
|
},
|
|
{
|
|
Key: "cloud_enabled",
|
|
DisplayName: "Cloud Storage Enabled",
|
|
Value: func(c *config.Config) string {
|
|
if c.CloudEnabled {
|
|
return "true"
|
|
}
|
|
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.CloudEnabled = val
|
|
return nil
|
|
},
|
|
Type: "bool",
|
|
Description: "Enable cloud storage integration (S3, Azure, GCS)",
|
|
},
|
|
{
|
|
Key: "cloud_provider",
|
|
DisplayName: "Cloud Provider",
|
|
Value: func(c *config.Config) string { return c.CloudProvider },
|
|
Update: func(c *config.Config, v string) error {
|
|
providers := []string{"s3", "minio", "b2", "azure", "gcs"}
|
|
currentIdx := -1
|
|
for i, p := range providers {
|
|
if c.CloudProvider == p {
|
|
currentIdx = i
|
|
break
|
|
}
|
|
}
|
|
nextIdx := (currentIdx + 1) % len(providers)
|
|
c.CloudProvider = providers[nextIdx]
|
|
return nil
|
|
},
|
|
Type: "selector",
|
|
Description: "Cloud storage provider (press Enter to cycle: S3 → MinIO → B2 → Azure → GCS)",
|
|
},
|
|
{
|
|
Key: "cloud_bucket",
|
|
DisplayName: "Cloud Bucket/Container",
|
|
Value: func(c *config.Config) string { return c.CloudBucket },
|
|
Update: func(c *config.Config, v string) error {
|
|
c.CloudBucket = v
|
|
return nil
|
|
},
|
|
Type: "string",
|
|
Description: "Bucket name (S3/GCS) or container name (Azure)",
|
|
},
|
|
{
|
|
Key: "cloud_region",
|
|
DisplayName: "Cloud Region",
|
|
Value: func(c *config.Config) string { return c.CloudRegion },
|
|
Update: func(c *config.Config, v string) error {
|
|
c.CloudRegion = v
|
|
return nil
|
|
},
|
|
Type: "string",
|
|
Description: "Region (e.g., us-east-1 for S3, us-central1 for GCS)",
|
|
},
|
|
{
|
|
Key: "cloud_access_key",
|
|
DisplayName: "Cloud Access Key",
|
|
Value: func(c *config.Config) string {
|
|
if c.CloudAccessKey != "" {
|
|
return "***" + c.CloudAccessKey[len(c.CloudAccessKey)-4:]
|
|
}
|
|
return ""
|
|
},
|
|
Update: func(c *config.Config, v string) error {
|
|
c.CloudAccessKey = v
|
|
return nil
|
|
},
|
|
Type: "string",
|
|
Description: "Access key (S3/MinIO), Account name (Azure), or Service account path (GCS)",
|
|
},
|
|
{
|
|
Key: "cloud_secret_key",
|
|
DisplayName: "Cloud Secret Key",
|
|
Value: func(c *config.Config) string {
|
|
if c.CloudSecretKey != "" {
|
|
return "********"
|
|
}
|
|
return ""
|
|
},
|
|
Update: func(c *config.Config, v string) error {
|
|
c.CloudSecretKey = v
|
|
return nil
|
|
},
|
|
Type: "string",
|
|
Description: "Secret key (S3/MinIO/B2) or Account key (Azure)",
|
|
},
|
|
{
|
|
Key: "cloud_auto_upload",
|
|
DisplayName: "Cloud Auto-Upload",
|
|
Value: func(c *config.Config) string {
|
|
if c.CloudAutoUpload {
|
|
return "true"
|
|
}
|
|
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.CloudAutoUpload = val
|
|
return nil
|
|
},
|
|
Type: "bool",
|
|
Description: "Automatically upload backups to cloud after creation",
|
|
},
|
|
}
|
|
|
|
return SettingsModel{
|
|
config: cfg,
|
|
logger: log,
|
|
settings: settings,
|
|
parent: parent,
|
|
}
|
|
}
|
|
|
|
// Init initializes the settings model
|
|
func (m SettingsModel) Init() tea.Cmd {
|
|
// Auto-forward in auto-confirm mode
|
|
if m.config.TUIAutoConfirm {
|
|
return func() tea.Msg {
|
|
return settingsAutoQuitMsg{}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// settingsAutoQuitMsg triggers automatic quit in settings
|
|
type settingsAutoQuitMsg struct{}
|
|
|
|
// Update handles messages
|
|
func (m SettingsModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
switch msg := msg.(type) {
|
|
case settingsAutoQuitMsg:
|
|
return m.parent, tea.Quit
|
|
|
|
case tea.KeyMsg:
|
|
// Handle directory browsing mode
|
|
if m.browsingDir && m.dirBrowser != nil {
|
|
switch msg.String() {
|
|
case "esc":
|
|
m.browsingDir = false
|
|
m.dirBrowser.Hide()
|
|
return m, nil
|
|
case "up", "k":
|
|
m.dirBrowser.Navigate(-1)
|
|
return m, nil
|
|
case "down", "j":
|
|
m.dirBrowser.Navigate(1)
|
|
return m, nil
|
|
case "enter", "right", "l":
|
|
m.dirBrowser.Enter()
|
|
return m, nil
|
|
case "left", "h":
|
|
// Go up one level (same as selecting ".." and entering)
|
|
parentPath := filepath.Dir(m.dirBrowser.CurrentPath)
|
|
if parentPath != m.dirBrowser.CurrentPath { // Avoid infinite loop at root
|
|
m.dirBrowser.CurrentPath = parentPath
|
|
m.dirBrowser.LoadItems()
|
|
}
|
|
return m, nil
|
|
case " ":
|
|
// Select current directory
|
|
selectedPath := m.dirBrowser.Select()
|
|
if m.cursor < len(m.settings) {
|
|
setting := m.settings[m.cursor]
|
|
if err := setting.Update(m.config, selectedPath); err != nil {
|
|
m.message = "[FAIL] Error: " + err.Error()
|
|
} else {
|
|
m.message = "[OK] Directory updated: " + selectedPath
|
|
}
|
|
}
|
|
m.browsingDir = false
|
|
m.dirBrowser.Hide()
|
|
return m, nil
|
|
case "tab":
|
|
// Toggle back to settings
|
|
m.browsingDir = false
|
|
m.dirBrowser.Hide()
|
|
return m, nil
|
|
}
|
|
return m, nil
|
|
}
|
|
|
|
if m.editing {
|
|
return m.handleEditingInput(msg)
|
|
}
|
|
|
|
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.settings)-1 {
|
|
m.cursor++
|
|
}
|
|
|
|
case "enter", " ":
|
|
// For selector types, cycle through options instead of typing
|
|
if m.cursor >= 0 && m.cursor < len(m.settings) {
|
|
currentSetting := m.settings[m.cursor]
|
|
if currentSetting.Type == "selector" {
|
|
if err := currentSetting.Update(m.config, ""); err != nil {
|
|
m.message = errorStyle.Render(fmt.Sprintf("[FAIL] %s", err.Error()))
|
|
} else {
|
|
m.message = successStyle.Render(fmt.Sprintf("[OK] Updated %s", currentSetting.DisplayName))
|
|
}
|
|
return m, nil
|
|
}
|
|
}
|
|
return m.startEditing()
|
|
|
|
case "tab":
|
|
// Directory browser for path fields
|
|
if m.cursor >= 0 && m.cursor < len(m.settings) {
|
|
if m.settings[m.cursor].Type == "path" {
|
|
return m.openDirectoryBrowser()
|
|
} else {
|
|
m.message = "[FAIL] Tab key only works on directory path fields"
|
|
return m, nil
|
|
}
|
|
} else {
|
|
m.message = "[FAIL] Invalid selection"
|
|
return m, nil
|
|
}
|
|
|
|
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", "ctrl+h":
|
|
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("[FAIL] 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("[FAIL] %s", err.Error()))
|
|
return m, nil
|
|
}
|
|
|
|
m.message = successStyle.Render(fmt.Sprintf("[OK] 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("[OK] 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("[FAIL] 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("[FAIL] CPU optimization failed: %s", err.Error()))
|
|
return m, nil
|
|
}
|
|
}
|
|
|
|
m.message = successStyle.Render("[OK] Settings validated and saved")
|
|
return m, nil
|
|
}
|
|
|
|
// cycleDatabaseType cycles through database type options
|
|
func (m SettingsModel) cycleDatabaseType() (tea.Model, tea.Cmd) {
|
|
dbTypes := []string{"postgres", "mysql", "mariadb"}
|
|
|
|
// Find current index
|
|
currentIdx := 0
|
|
for i, dbType := range dbTypes {
|
|
if m.config.DatabaseType == dbType {
|
|
currentIdx = i
|
|
break
|
|
}
|
|
}
|
|
|
|
// Cycle to next
|
|
nextIdx := (currentIdx + 1) % len(dbTypes)
|
|
newType := dbTypes[nextIdx]
|
|
|
|
// Update config
|
|
if err := m.config.SetDatabaseType(newType); err != nil {
|
|
m.message = errorStyle.Render(fmt.Sprintf("[FAIL] Failed to set database type: %s", err.Error()))
|
|
return m, nil
|
|
}
|
|
|
|
m.message = successStyle.Render(fmt.Sprintf("[OK] Database type set to %s", m.config.DisplayDatabaseType()))
|
|
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("[CONFIG] 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)
|
|
displayValue := value
|
|
if setting.Key == "database_type" {
|
|
displayValue = fmt.Sprintf("%s (%s)", value, m.config.DisplayDatabaseType())
|
|
}
|
|
|
|
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(" [EDIT]")
|
|
} else {
|
|
line := fmt.Sprintf("%s %s: %s", cursor, setting.DisplayName, displayValue)
|
|
b.WriteString(selectedStyle.Render(line))
|
|
}
|
|
} else {
|
|
line := fmt.Sprintf("%s %s: %s", cursor, setting.DisplayName, displayValue)
|
|
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")
|
|
}
|
|
|
|
// Show directory browser for current path field
|
|
if m.cursor == i && m.browsingDir && m.dirBrowser != nil && setting.Type == "path" {
|
|
b.WriteString("\n")
|
|
browserView := m.dirBrowser.Render()
|
|
b.WriteString(browserView)
|
|
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("[LOG] Current Configuration:"))
|
|
b.WriteString("\n")
|
|
|
|
summary := []string{
|
|
fmt.Sprintf("Target DB: %s (%s)", m.config.DisplayDatabaseType(), m.config.DatabaseType),
|
|
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),
|
|
}
|
|
|
|
if m.config.CloudEnabled {
|
|
cloudInfo := fmt.Sprintf("Cloud: %s (%s)", m.config.CloudProvider, m.config.CloudBucket)
|
|
if m.config.CloudAutoUpload {
|
|
cloudInfo += " [auto-upload]"
|
|
}
|
|
summary = append(summary, cloudInfo)
|
|
}
|
|
|
|
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[KEYS] Type new value | Enter to save | Esc to cancel")
|
|
} else {
|
|
if m.browsingDir {
|
|
footer = infoStyle.Render("\n[KEYS] Up/Down navigate directories | Enter open | Space select | Tab/Esc back to settings")
|
|
} else {
|
|
// Show different help based on current selection
|
|
if m.cursor >= 0 && m.cursor < len(m.settings) && m.settings[m.cursor].Type == "path" {
|
|
footer = infoStyle.Render("\n[KEYS] Up/Down navigate | Enter edit | Tab browse directories | 's' save | 'r' reset | 'q' menu")
|
|
} else {
|
|
footer = infoStyle.Render("\n[KEYS] Up/Down navigate | Enter edit | 's' save | 'r' reset | 'q' menu | Tab=dirs on path fields only")
|
|
}
|
|
}
|
|
}
|
|
b.WriteString(footer)
|
|
|
|
return b.String()
|
|
}
|
|
|
|
func (m SettingsModel) openDirectoryBrowser() (tea.Model, tea.Cmd) {
|
|
if m.cursor >= len(m.settings) {
|
|
return m, nil
|
|
}
|
|
|
|
setting := m.settings[m.cursor]
|
|
currentValue := setting.Value(m.config)
|
|
if currentValue == "" {
|
|
currentValue = m.config.GetEffectiveWorkDir()
|
|
}
|
|
|
|
if m.dirBrowser == nil {
|
|
m.dirBrowser = NewDirectoryBrowser(currentValue)
|
|
} else {
|
|
// Update the browser to start from the current value
|
|
m.dirBrowser.CurrentPath = currentValue
|
|
m.dirBrowser.LoadItems()
|
|
}
|
|
|
|
m.dirBrowser.Show()
|
|
m.browsingDir = true
|
|
|
|
return m, nil
|
|
}
|
|
|
|
// 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
|
|
}
|