package ui import ( "fmt" "strings" "github.com/gdamore/tcell/v2" "github.com/rivo/tview" ) // connBuilderPage is the page name of the connection string builder dialog. const connBuilderPage = "conn-builder" // showConnStringBuilder opens the connection string builder, pre-filled by // parsing current. Save calls onDone with the built string; Esc/Back leaves // the caller's input untouched. func (se *SchemaEditor) showConnStringBuilder(current string, hint ConnKind, returnPage string, onDone func(connString string)) { fields, err := ParseConnString(current, hint) if err != nil { se.showErrorDialog("Error", err.Error()+"\nStarting from defaults.") } title := tview.NewTextView(). SetText("[::b]Connection String Builder"). SetTextAlign(tview.AlignCenter). SetDynamicColors(true) preview := tview.NewTextView() preview.SetBorder(true).SetTitle(" Preview (password masked) ").SetTitleAlign(tview.AlignLeft) form := tview.NewForm() form.SetBorder(true).SetTitle(" Connection ").SetTitleAlign(tview.AlignLeft) updatePreview := func() { preview.SetText(tview.Escape(BuildConnString(fields, true))) } closeBuilder := func() { se.pages.RemovePage(connBuilderPage) se.pages.SwitchToPage(returnPage) } var render func(focus int) render = func(focus int) { form.Clear(false) kindIndex := 0 kindLabels := make([]string, len(connKinds)) for i, k := range connKinds { kindLabels[i] = string(k) if k == fields.Kind { kindIndex = i } } form.AddDropDown("Type", kindLabels, kindIndex, func(_ string, index int) { if connKinds[index] == fields.Kind { return } fields = DefaultConnFields(connKinds[index]) render(0) }) if fields.Kind == ConnSQLite { form.AddInputField("File Path", fields.FilePath, 50, nil, func(v string) { fields.FilePath = v updatePreview() }) if item, ok := form.GetFormItemByLabel("File Path").(*tview.InputField); ok { item.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { if event.Key() != tcell.KeyEnter { return event } se.showFileBrowser(FileBrowserConfig{ Mode: FileBrowserLoad, StartPath: fields.FilePath, Extensions: FormatExtensions("sqlite"), ReturnPage: connBuilderPage, OnSelect: func(path string) { item.SetText(path) }, }) return nil }) } } else { form.AddInputField("Host", fields.Host, 50, nil, func(v string) { fields.Host = v; updatePreview() }) form.AddInputField("Port", fields.Port, 10, tview.InputFieldInteger, func(v string) { fields.Port = v; updatePreview() }) form.AddInputField("Database", fields.Database, 50, nil, func(v string) { fields.Database = v; updatePreview() }) form.AddInputField("User", fields.User, 50, nil, func(v string) { fields.User = v; updatePreview() }) form.AddPasswordField("Password", fields.Password, 50, '*', func(v string) { fields.Password = v; updatePreview() }) label := "SSL Mode" if fields.Kind == ConnMSSQL { label = "Encrypt" } modes := SSLModes(fields.Kind) modeIndex := -1 for i, m := range modes { if m == fields.SSLMode { modeIndex = i } } if modeIndex < 0 { // Keep a value parsed from an existing string even if it is not a listed option. modes = append([]string{fields.SSLMode}, modes...) modeIndex = 0 } form.AddDropDown(label, modes, modeIndex, func(option string, _ int) { fields.SSLMode = option updatePreview() }) } form.AddButton("Save [F2]", connBuilderSave(se, &fields, closeBuilder, onDone)) form.AddButton("Test [F3]", func() { se.testConnectionDialog(fields) }) form.AddButton("Back [Esc]", closeBuilder) updatePreview() form.SetFocus(focus) se.app.SetFocus(form) } form.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { switch event.Key() { case tcell.KeyEscape: closeBuilder() return nil case tcell.KeyF2: connBuilderSave(se, &fields, closeBuilder, onDone)() return nil case tcell.KeyF3: se.testConnectionDialog(fields) return nil } return event }) render(0) flex := tview.NewFlex().SetDirection(tview.FlexRow). AddItem(title, 1, 0, false). AddItem(form, 0, 1, true). AddItem(preview, 4, 0, false) se.pages.AddAndSwitchToPage(connBuilderPage, flex, true) se.app.SetFocus(form) } // connBuilderSave returns the Save action: validate, write back, close. func connBuilderSave(se *SchemaEditor, fields *ConnFields, closeBuilder func(), onDone func(string)) func() { return func() { if msg := validateConnFields(*fields); msg != "" { se.showErrorDialog("Error", msg) return } result := BuildConnString(*fields, false) closeBuilder() onDone(result) } } // validateConnFields returns a message describing the first missing required field, or "". func validateConnFields(f ConnFields) string { if f.Kind == ConnSQLite { if strings.TrimSpace(f.FilePath) == "" { return "File path is required" } return "" } if strings.TrimSpace(f.Host) == "" { return "Host is required" } return "" } // testConnectionDialog runs TestConnection in the background and reports the result. func (se *SchemaEditor) testConnectionDialog(fields ConnFields) { if msg := validateConnFields(fields); msg != "" { se.showErrorDialog("Error", msg) return } go func() { err := TestConnection(fields) se.app.QueueUpdateDraw(func() { if err != nil { se.showErrorDialog("Connection Failed", fmt.Sprintf("Connection failed:\n%v", err)) return } se.showSuccessDialog("Connection OK", "Connection successful", nil) }) }() } // attachConnStringBuilder makes Enter on the named input open the builder. func (se *SchemaEditor) attachConnStringBuilder(form *tview.Form, label, returnPage string, format func() string) { item, ok := form.GetFormItemByLabel(label).(*tview.InputField) if !ok { return } item.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { if event.Key() != tcell.KeyEnter { return event } hint := ConnPostgres if format != nil && format() == "sqlite" { hint = ConnSQLite } se.showConnStringBuilder(item.GetText(), hint, returnPage, func(s string) { item.SetText(s) }) return nil }) }