package ui import ( "fmt" "os" "path/filepath" "github.com/gdamore/tcell/v2" "github.com/rivo/tview" "git.warky.dev/wdevs/relspecgo/pkg/models" "git.warky.dev/wdevs/relspecgo/pkg/readers" rbun "git.warky.dev/wdevs/relspecgo/pkg/readers/bun" rdbml "git.warky.dev/wdevs/relspecgo/pkg/readers/dbml" rdctx "git.warky.dev/wdevs/relspecgo/pkg/readers/dctx" rdrawdb "git.warky.dev/wdevs/relspecgo/pkg/readers/drawdb" rdrizzle "git.warky.dev/wdevs/relspecgo/pkg/readers/drizzle" rgorm "git.warky.dev/wdevs/relspecgo/pkg/readers/gorm" rgraphql "git.warky.dev/wdevs/relspecgo/pkg/readers/graphql" rjson "git.warky.dev/wdevs/relspecgo/pkg/readers/json" rpgsql "git.warky.dev/wdevs/relspecgo/pkg/readers/pgsql" rprisma "git.warky.dev/wdevs/relspecgo/pkg/readers/prisma" rtypeorm "git.warky.dev/wdevs/relspecgo/pkg/readers/typeorm" ryaml "git.warky.dev/wdevs/relspecgo/pkg/readers/yaml" "git.warky.dev/wdevs/relspecgo/pkg/writers" wbun "git.warky.dev/wdevs/relspecgo/pkg/writers/bun" wdbml "git.warky.dev/wdevs/relspecgo/pkg/writers/dbml" wdctx "git.warky.dev/wdevs/relspecgo/pkg/writers/dctx" wdrawdb "git.warky.dev/wdevs/relspecgo/pkg/writers/drawdb" wdrizzle "git.warky.dev/wdevs/relspecgo/pkg/writers/drizzle" wgorm "git.warky.dev/wdevs/relspecgo/pkg/writers/gorm" wgraphql "git.warky.dev/wdevs/relspecgo/pkg/writers/graphql" wjson "git.warky.dev/wdevs/relspecgo/pkg/writers/json" wpgsql "git.warky.dev/wdevs/relspecgo/pkg/writers/pgsql" wprisma "git.warky.dev/wdevs/relspecgo/pkg/writers/prisma" wtypeorm "git.warky.dev/wdevs/relspecgo/pkg/writers/typeorm" wyaml "git.warky.dev/wdevs/relspecgo/pkg/writers/yaml" ) // LoadConfig holds the configuration for loading a database type LoadConfig struct { SourceType string FilePath string ConnString string } // SaveConfig holds the configuration for saving a database type SaveConfig struct { TargetType string FilePath string ConnString string } // showLoadScreen displays the database load screen func (se *SchemaEditor) showLoadScreen() { flex := tview.NewFlex().SetDirection(tview.FlexRow) // Title title := tview.NewTextView(). SetText("[::b]Load Database Schema"). SetTextAlign(tview.AlignCenter). SetDynamicColors(true) // Form form := tview.NewForm() form.SetBorder(true).SetTitle(" Load Configuration ").SetTitleAlign(tview.AlignLeft) // Format selection formatOptions := []string{ "dbml", "dctx", "drawdb", "graphql", "json", "yaml", "gorm", "bun", "drizzle", "prisma", "typeorm", "pgsql", } selectedFormat := 0 currentFormat := formatOptions[selectedFormat] // File path input filePath := "" connString := "" form.AddDropDown("Format", formatOptions, 0, func(option string, index int) { selectedFormat = index currentFormat = option }) form.AddInputField("File Path", "", 50, nil, func(value string) { filePath = value }) form.AddInputField("Connection String", "", 50, nil, func(value string) { connString = value }) form.AddTextView("Help", getLoadHelpText(), 0, 5, true, false) // Buttons form.AddButton("Load [l]", func() { se.loadDatabase(currentFormat, filePath, connString) }) form.AddButton("Create New [n]", func() { se.createNewDatabase() }) form.AddButton("Exit [q]", func() { se.app.Stop() }) // Keyboard shortcuts form.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { if event.Key() == tcell.KeyEscape { se.app.Stop() return nil } switch event.Rune() { case 'l': se.loadDatabase(currentFormat, filePath, connString) return nil case 'n': se.createNewDatabase() return nil case 'q': se.app.Stop() return nil } return event }) // Tab navigation form.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { if event.Key() == tcell.KeyEscape { se.app.Stop() return nil } if event.Rune() == 'l' || event.Rune() == 'n' || event.Rune() == 'q' { return event } return event }) flex.AddItem(title, 1, 0, false). AddItem(form, 0, 1, true) se.pages.AddAndSwitchToPage("load-database", flex, true) } // showSaveScreen displays the save database screen func (se *SchemaEditor) showSaveScreen() { flex := tview.NewFlex().SetDirection(tview.FlexRow) // Title title := tview.NewTextView(). SetText("[::b]Save Database Schema"). SetTextAlign(tview.AlignCenter). SetDynamicColors(true) // Form form := tview.NewForm() form.SetBorder(true).SetTitle(" Save Configuration ").SetTitleAlign(tview.AlignLeft) // Format selection formatOptions := []string{ "dbml", "dctx", "drawdb", "graphql", "json", "yaml", "gorm", "bun", "drizzle", "prisma", "typeorm", "pgsql", } selectedFormat := 0 currentFormat := formatOptions[selectedFormat] // File path input filePath := "" if se.saveConfig != nil { // Pre-populate with existing save config for i, format := range formatOptions { if format == se.saveConfig.TargetType { selectedFormat = i currentFormat = format break } } filePath = se.saveConfig.FilePath } form.AddDropDown("Format", formatOptions, selectedFormat, func(option string, index int) { selectedFormat = index currentFormat = option }) form.AddInputField("File Path", filePath, 50, nil, func(value string) { filePath = value }) form.AddTextView("Help", getSaveHelpText(), 0, 5, true, false) // Buttons form.AddButton("Save [s]", func() { se.saveDatabase(currentFormat, filePath) }) form.AddButton("Update Existing Database [u]", func() { // Use saveConfig if available, otherwise use loadConfig if se.saveConfig != nil { se.showUpdateExistingDatabaseConfirm() } else if se.loadConfig != nil { se.showUpdateExistingDatabaseConfirm() } else { se.showErrorDialog("Error", "No database source found. Use Save instead.") } }) form.AddButton("Back [b]", func() { se.pages.RemovePage("save-database") se.pages.SwitchToPage("main") }) // Keyboard shortcuts form.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { if event.Key() == tcell.KeyEscape { se.pages.RemovePage("save-database") se.pages.SwitchToPage("main") return nil } switch event.Rune() { case 's': se.saveDatabase(currentFormat, filePath) return nil case 'u': // Use saveConfig if available, otherwise use loadConfig if se.saveConfig != nil { se.showUpdateExistingDatabaseConfirm() } else if se.loadConfig != nil { se.showUpdateExistingDatabaseConfirm() } else { se.showErrorDialog("Error", "No database source found. Use Save instead.") } return nil case 'b': se.pages.RemovePage("save-database") se.pages.SwitchToPage("main") return nil } return event }) flex.AddItem(title, 1, 0, false). AddItem(form, 0, 1, true) se.pages.AddAndSwitchToPage("save-database", flex, true) } // loadDatabase loads a database from the specified configuration func (se *SchemaEditor) loadDatabase(format, filePath, connString string) { // Validate input if format == "pgsql" { if connString == "" { se.showErrorDialog("Error", "Connection string is required for PostgreSQL") return } } else { if filePath == "" { se.showErrorDialog("Error", "File path is required for "+format) return } // Expand home directory if len(filePath) > 0 && filePath[0] == '~' { home, err := os.UserHomeDir() if err == nil { filePath = filepath.Join(home, filePath[1:]) } } } // Create reader var reader readers.Reader switch format { case "dbml": reader = rdbml.NewReader(&readers.ReaderOptions{FilePath: filePath}) case "dctx": reader = rdctx.NewReader(&readers.ReaderOptions{FilePath: filePath}) case "drawdb": reader = rdrawdb.NewReader(&readers.ReaderOptions{FilePath: filePath}) case "graphql": reader = rgraphql.NewReader(&readers.ReaderOptions{FilePath: filePath}) case "json": reader = rjson.NewReader(&readers.ReaderOptions{FilePath: filePath}) case "yaml": reader = ryaml.NewReader(&readers.ReaderOptions{FilePath: filePath}) case "gorm": reader = rgorm.NewReader(&readers.ReaderOptions{FilePath: filePath}) case "bun": reader = rbun.NewReader(&readers.ReaderOptions{FilePath: filePath}) case "drizzle": reader = rdrizzle.NewReader(&readers.ReaderOptions{FilePath: filePath}) case "prisma": reader = rprisma.NewReader(&readers.ReaderOptions{FilePath: filePath}) case "typeorm": reader = rtypeorm.NewReader(&readers.ReaderOptions{FilePath: filePath}) case "pgsql": reader = rpgsql.NewReader(&readers.ReaderOptions{ConnectionString: connString}) default: se.showErrorDialog("Error", "Unsupported format: "+format) return } // Read database db, err := reader.ReadDatabase() if err != nil { se.showErrorDialog("Load Error", fmt.Sprintf("Failed to load database: %v", err)) return } // Store load config se.loadConfig = &LoadConfig{ SourceType: format, FilePath: filePath, ConnString: connString, } // Update database se.db = db // Show success and switch to main menu se.showSuccessDialog("Load Complete", fmt.Sprintf("Successfully loaded database '%s'", db.Name), func() { se.pages.RemovePage("load-database") se.pages.RemovePage("main") se.pages.AddPage("main", se.createMainMenu(), true, true) }) } // saveDatabase saves the database to the specified configuration func (se *SchemaEditor) saveDatabase(format, filePath string) { // Validate input if format == "pgsql" { se.showErrorDialog("Error", "Direct PostgreSQL save is not supported from the UI. Use --to pgsql --to-path output.sql") return } if filePath == "" { se.showErrorDialog("Error", "File path is required") return } // Expand home directory if len(filePath) > 0 && filePath[0] == '~' { home, err := os.UserHomeDir() if err == nil { filePath = filepath.Join(home, filePath[1:]) } } // Create writer var writer writers.Writer switch format { case "dbml": writer = wdbml.NewWriter(&writers.WriterOptions{OutputPath: filePath}) case "dctx": writer = wdctx.NewWriter(&writers.WriterOptions{OutputPath: filePath}) case "drawdb": writer = wdrawdb.NewWriter(&writers.WriterOptions{OutputPath: filePath}) case "graphql": writer = wgraphql.NewWriter(&writers.WriterOptions{OutputPath: filePath}) case "json": writer = wjson.NewWriter(&writers.WriterOptions{OutputPath: filePath}) case "yaml": writer = wyaml.NewWriter(&writers.WriterOptions{OutputPath: filePath}) case "gorm": writer = wgorm.NewWriter(&writers.WriterOptions{OutputPath: filePath}) case "bun": writer = wbun.NewWriter(&writers.WriterOptions{OutputPath: filePath}) case "drizzle": writer = wdrizzle.NewWriter(&writers.WriterOptions{OutputPath: filePath}) case "prisma": writer = wprisma.NewWriter(&writers.WriterOptions{OutputPath: filePath}) case "typeorm": writer = wtypeorm.NewWriter(&writers.WriterOptions{OutputPath: filePath}) case "pgsql": writer = wpgsql.NewWriter(&writers.WriterOptions{OutputPath: filePath}) default: se.showErrorDialog("Error", "Unsupported format: "+format) return } // Write database err := writer.WriteDatabase(se.db) if err != nil { se.showErrorDialog("Save Error", fmt.Sprintf("Failed to save database: %v", err)) return } // Store save config se.saveConfig = &SaveConfig{ TargetType: format, FilePath: filePath, } // Show success se.showSuccessDialog("Save Complete", fmt.Sprintf("Successfully saved database to %s", filePath), func() { se.pages.RemovePage("save-database") se.pages.SwitchToPage("main") }) } // createNewDatabase creates a new empty database func (se *SchemaEditor) createNewDatabase() { // Create a new empty database se.db = &models.Database{ Name: "New Database", Schemas: []*models.Schema{}, } // Clear load config se.loadConfig = nil // Show success and switch to main menu se.showSuccessDialog("New Database", "Created new empty database", func() { se.pages.RemovePage("load-database") se.pages.AddPage("main", se.createMainMenu(), true, true) }) } // showErrorDialog displays an error dialog func (se *SchemaEditor) showErrorDialog(_title, message string) { modal := tview.NewModal(). SetText(message). AddButtons([]string{"OK"}). SetDoneFunc(func(buttonIndex int, buttonLabel string) { se.pages.RemovePage("error-dialog") }) modal.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { if event.Key() == tcell.KeyEscape { se.pages.RemovePage("error-dialog") return nil } return event }) se.pages.AddPage("error-dialog", modal, true, true) } // showSuccessDialog displays a success dialog func (se *SchemaEditor) showSuccessDialog(_title, message string, onClose func()) { modal := tview.NewModal(). SetText(message). AddButtons([]string{"OK"}). SetDoneFunc(func(buttonIndex int, buttonLabel string) { se.pages.RemovePage("success-dialog") if onClose != nil { onClose() } }) modal.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { if event.Key() == tcell.KeyEscape { se.pages.RemovePage("success-dialog") if onClose != nil { onClose() } return nil } return event }) se.pages.AddPage("success-dialog", modal, true, true) } // getLoadHelpText returns the help text for the load screen func getLoadHelpText() string { return `File-based formats: dbml, dctx, drawdb, graphql, json, yaml, gorm, bun, drizzle, prisma, typeorm Database formats: pgsql (requires connection string) Examples: - File path: ~/schemas/mydb.dbml or /path/to/schema.json - Connection: postgres://user:pass@localhost/dbname` } // showUpdateExistingDatabaseConfirm displays a confirmation dialog before updating existing database func (se *SchemaEditor) showUpdateExistingDatabaseConfirm() { // Use saveConfig if available, otherwise use loadConfig var targetType, targetPath string if se.saveConfig != nil { targetType = se.saveConfig.TargetType targetPath = se.saveConfig.FilePath } else if se.loadConfig != nil { targetType = se.loadConfig.SourceType targetPath = se.loadConfig.FilePath } else { return } confirmText := fmt.Sprintf("Update existing database?\n\nFormat: %s\nPath: %s\n\nThis will overwrite the source.", targetType, targetPath) modal := tview.NewModal(). SetText(confirmText). AddButtons([]string{"Cancel", "Update"}). SetDoneFunc(func(buttonIndex int, buttonLabel string) { if buttonLabel == "Update" { se.pages.RemovePage("update-confirm") se.pages.RemovePage("save-database") se.saveDatabase(targetType, targetPath) se.pages.SwitchToPage("main") } else { se.pages.RemovePage("update-confirm") } }) modal.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { if event.Key() == tcell.KeyEscape { se.pages.RemovePage("update-confirm") return nil } return event }) se.pages.AddAndSwitchToPage("update-confirm", modal, true) } // getSaveHelpText returns the help text for the save screen func getSaveHelpText() string { return `File-based formats: dbml, dctx, drawdb, graphql, json, yaml, gorm, bun, drizzle, prisma, typeorm, pgsql (SQL export) Examples: - File: ~/schemas/mydb.dbml - Directory (for code formats): ./models/` }