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.EqualFold(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, "" }