Enter on File Path inputs opens a file browser (load/save, extension filter, hidden toggle, overwrite confirm). Enter on Connection String inputs opens a builder for PostgreSQL, MSSQL and SQLite with masked password/preview, parsing and optional connection test. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
211 lines
6.1 KiB
Go
211 lines
6.1 KiB
Go
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
|
|
})
|
|
}
|