feat(ui): file browser and connection string builder dialogs (#44)
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>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
b38f53c603
commit
0235a28add
@@ -0,0 +1,134 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// FileEntry is a single row in the file browser.
|
||||
type FileEntry struct {
|
||||
Name string
|
||||
IsDir bool
|
||||
}
|
||||
|
||||
// formatExtensions maps a UI format name to the file extensions it reads or writes.
|
||||
var formatExtensions = map[string][]string{
|
||||
"dbml": {".dbml"},
|
||||
"dctx": {".dctx"},
|
||||
"drawdb": {".json"},
|
||||
"graphql": {".graphql", ".gql"},
|
||||
"json": {".json"},
|
||||
"yaml": {".yaml", ".yml"},
|
||||
"gorm": {".go"},
|
||||
"bun": {".go"},
|
||||
"drizzle": {".ts"},
|
||||
"prisma": {".prisma"},
|
||||
"typeorm": {".ts"},
|
||||
"pgsql": {".sql"},
|
||||
"sqlite": {".db", ".sqlite", ".sqlite3"},
|
||||
}
|
||||
|
||||
// directoryFormats are formats whose reader/writer accepts a directory.
|
||||
var directoryFormats = map[string]bool{
|
||||
"gorm": true, "bun": true, "drizzle": true, "typeorm": true,
|
||||
}
|
||||
|
||||
// FormatExtensions returns the extensions for a format, or nil (no filter) if unknown.
|
||||
func FormatExtensions(format string) []string {
|
||||
return formatExtensions[format]
|
||||
}
|
||||
|
||||
// IsDirectoryFormat reports whether a format can be loaded from or saved to a directory.
|
||||
func IsDirectoryFormat(format string) bool {
|
||||
return directoryFormats[format]
|
||||
}
|
||||
|
||||
// ExpandHome replaces a leading ~ with the user's home directory.
|
||||
func ExpandHome(p string) string {
|
||||
if strings.HasPrefix(p, "~") {
|
||||
if home, err := os.UserHomeDir(); err == nil {
|
||||
return filepath.Join(home, p[1:])
|
||||
}
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
// MatchesExtension reports whether name has one of exts (case-insensitive).
|
||||
// An empty extension list matches everything.
|
||||
func MatchesExtension(name string, exts []string) bool {
|
||||
if len(exts) == 0 {
|
||||
return true
|
||||
}
|
||||
ext := strings.ToLower(filepath.Ext(name))
|
||||
for _, e := range exts {
|
||||
if strings.ToLower(e) == ext {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ListDir returns the entries of dir: directories first, then files that match
|
||||
// exts, each group sorted case-insensitively. Hidden (dot) entries are skipped
|
||||
// unless showHidden is set.
|
||||
func ListDir(dir string, exts []string, showHidden bool) ([]FileEntry, error) {
|
||||
items, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var dirs, files []FileEntry
|
||||
for _, item := range items {
|
||||
name := item.Name()
|
||||
if !showHidden && strings.HasPrefix(name, ".") {
|
||||
continue
|
||||
}
|
||||
isDir := item.IsDir()
|
||||
if !isDir && item.Type()&os.ModeSymlink != 0 {
|
||||
// Follow symlinks so links to directories are navigable.
|
||||
if info, err := os.Stat(filepath.Join(dir, name)); err == nil {
|
||||
isDir = info.IsDir()
|
||||
}
|
||||
}
|
||||
if isDir {
|
||||
dirs = append(dirs, FileEntry{Name: name, IsDir: true})
|
||||
} else if MatchesExtension(name, exts) {
|
||||
files = append(files, FileEntry{Name: name})
|
||||
}
|
||||
}
|
||||
|
||||
byName := func(s []FileEntry) {
|
||||
sort.Slice(s, func(i, j int) bool {
|
||||
return strings.ToLower(s[i].Name) < strings.ToLower(s[j].Name)
|
||||
})
|
||||
}
|
||||
byName(dirs)
|
||||
byName(files)
|
||||
return append(dirs, files...), nil
|
||||
}
|
||||
|
||||
// ResolveStart works out where the browser should open for the current input
|
||||
// value. It returns the directory to show and, if the input named a file, its
|
||||
// base name. Falls back to the working directory.
|
||||
func ResolveStart(input string) (dir, name string) {
|
||||
input = strings.TrimSpace(input)
|
||||
if input != "" {
|
||||
p := ExpandHome(input)
|
||||
if abs, err := filepath.Abs(p); err == nil {
|
||||
p = abs
|
||||
}
|
||||
if info, err := os.Stat(p); err == nil && info.IsDir() {
|
||||
return p, ""
|
||||
}
|
||||
if info, err := os.Stat(filepath.Dir(p)); err == nil && info.IsDir() {
|
||||
return filepath.Dir(p), filepath.Base(p)
|
||||
}
|
||||
}
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
wd = "."
|
||||
}
|
||||
return wd, ""
|
||||
}
|
||||
Reference in New Issue
Block a user