aboutsummaryrefslogtreecommitdiff
path: root/components/shared/filepicker.go
diff options
context:
space:
mode:
authorPriyansh <[email protected]>2025-08-28 13:09:22 +0530
committerPriyansh <[email protected]>2025-08-28 13:09:22 +0530
commit9e82811a2a963be962fc3ffc426c137e01d56e2d (patch)
tree9cde1691f6e8dafb7204b7247dea12af0aa22bf6 /components/shared/filepicker.go
parent18b897a36805d1acb6e2352ca536c1eee9249fe3 (diff)
downloadnectar-9e82811a2a963be962fc3ffc426c137e01d56e2d.tar.xz
nectar-9e82811a2a963be962fc3ffc426c137e01d56e2d.zip
Restructure codebase with proper organization, fix file picker navigation, and add utils packageHEADmain
Diffstat (limited to 'components/shared/filepicker.go')
-rw-r--r--components/shared/filepicker.go232
1 files changed, 232 insertions, 0 deletions
diff --git a/components/shared/filepicker.go b/components/shared/filepicker.go
new file mode 100644
index 0000000..a2f2062
--- /dev/null
+++ b/components/shared/filepicker.go
@@ -0,0 +1,232 @@
+package shared
+
+import (
+ "io/fs"
+ "os"
+ "path/filepath"
+ "sort"
+ "strings"
+
+ tea "github.com/charmbracelet/bubbletea"
+ "github.com/charmbracelet/lipgloss"
+)
+
+const (
+ MaxVisibleFiles = 10
+)
+
+type FilePickerModel struct {
+ currentDir string
+ files []fs.DirEntry
+ selected int
+ selectedFile string
+ err error
+ scrollOffset int
+}
+
+func NewFilePicker() FilePickerModel {
+ homeDir, err := os.UserHomeDir()
+ if err != nil {
+ homeDir = "."
+ }
+
+ fp := FilePickerModel{
+ currentDir: homeDir,
+ selected: 0,
+ scrollOffset: 0,
+ }
+ fp.loadDirectory()
+ return fp
+}
+
+func (m *FilePickerModel) loadDirectory() {
+ files, err := os.ReadDir(m.currentDir)
+ if err != nil {
+ m.err = err
+ m.files = nil
+ return
+ }
+
+ var filteredFiles []fs.DirEntry
+
+ // Always add parent directory option unless we're at root
+ parentDir := filepath.Dir(m.currentDir)
+ if parentDir != m.currentDir {
+ // Create a synthetic directory entry for ".."
+ filteredFiles = append(filteredFiles, &parentDirEntry{})
+ }
+
+ // Add directories and SQLite files
+ for _, file := range files {
+ if file.IsDir() {
+ filteredFiles = append(filteredFiles, file)
+ } else if strings.HasSuffix(strings.ToLower(file.Name()), ".db") ||
+ strings.HasSuffix(strings.ToLower(file.Name()), ".sqlite") ||
+ strings.HasSuffix(strings.ToLower(file.Name()), ".sqlite3") {
+ filteredFiles = append(filteredFiles, file)
+ }
+ }
+
+ // Sort: parent directory first, then directories, then files
+ sort.Slice(filteredFiles, func(i, j int) bool {
+ nameI := filteredFiles[i].Name()
+ nameJ := filteredFiles[j].Name()
+
+ // Parent directory always comes first
+ if nameI == ".." {
+ return true
+ }
+ if nameJ == ".." {
+ return false
+ }
+
+ // Then sort by type (directories first) and name
+ if filteredFiles[i].IsDir() != filteredFiles[j].IsDir() {
+ return filteredFiles[i].IsDir()
+ }
+ return nameI < nameJ
+ })
+
+ m.files = filteredFiles
+ m.selected = 0
+ m.scrollOffset = 0
+ m.err = nil
+}
+
+// parentDirEntry implements fs.DirEntry for the ".." parent directory
+type parentDirEntry struct{}
+
+func (p *parentDirEntry) Name() string { return ".." }
+func (p *parentDirEntry) IsDir() bool { return true }
+func (p *parentDirEntry) Type() fs.FileMode { return fs.ModeDir }
+func (p *parentDirEntry) Info() (fs.FileInfo, error) { return nil, nil }
+
+func (m FilePickerModel) Init() tea.Cmd {
+ return nil
+}
+
+func (m FilePickerModel) Update(msg tea.Msg) (FilePickerModel, tea.Cmd) {
+ switch msg := msg.(type) {
+ case tea.KeyMsg:
+ switch msg.String() {
+ case "up", "k":
+ if m.selected > 0 {
+ m.selected--
+ // Adjust scroll offset to keep selection visible
+ if m.selected < m.scrollOffset {
+ m.scrollOffset = m.selected
+ }
+ }
+ case "down", "j":
+ if m.selected < len(m.files)-1 {
+ m.selected++
+ // Adjust scroll offset to keep selection visible
+ if m.selected >= m.scrollOffset+MaxVisibleFiles {
+ m.scrollOffset = m.selected - MaxVisibleFiles + 1
+ }
+ }
+ case "enter":
+ if len(m.files) == 0 {
+ break
+ }
+ selectedFile := m.files[m.selected]
+ if selectedFile.IsDir() {
+ if selectedFile.Name() == ".." {
+ // Navigate to parent directory
+ m.currentDir = filepath.Dir(m.currentDir)
+ } else {
+ // Navigate to selected directory
+ m.currentDir = filepath.Join(m.currentDir, selectedFile.Name())
+ }
+ m.loadDirectory()
+ } else {
+ // Select the SQLite file
+ m.selectedFile = filepath.Join(m.currentDir, selectedFile.Name())
+ }
+ }
+ }
+ return m, nil
+}
+
+func (m FilePickerModel) View() string {
+ // Fixed height container to prevent UI pushing
+ containerStyle := lipgloss.NewStyle().
+ Border(lipgloss.RoundedBorder()).
+ Padding(1, 2).
+ Width(80).
+ Height(MaxVisibleFiles + 8) // Fixed height: header + files + help + padding
+
+ if m.err != nil {
+ errorContent := "Select SQLite Database File\n\nError: " + m.err.Error()
+ return containerStyle.Render(errorContent)
+ }
+
+ var content strings.Builder
+
+ // Header section
+ headerStyle := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("12"))
+ content.WriteString(headerStyle.Render("Select SQLite Database File"))
+ content.WriteString("\n")
+ content.WriteString("Current: " + filepath.Base(m.currentDir))
+ content.WriteString("\n\n")
+
+ // Files section with fixed height
+ if len(m.files) == 0 {
+ content.WriteString("No SQLite files found in this directory")
+ // Add padding to maintain consistent height
+ for i := 0; i < MaxVisibleFiles-1; i++ {
+ content.WriteString("\n")
+ }
+ } else {
+ visibleStart := m.scrollOffset
+ visibleEnd := min(visibleStart+MaxVisibleFiles, len(m.files))
+
+ // Display visible files
+ for i := visibleStart; i < visibleEnd; i++ {
+ file := m.files[i]
+ var line string
+
+ if file.Name() == ".." {
+ line = "📁 .. (parent directory)"
+ } else if file.IsDir() {
+ line = "📁 " + file.Name() + "/"
+ } else {
+ line = "📄 " + file.Name()
+ }
+
+ if i == m.selected {
+ selectedStyle := lipgloss.NewStyle().
+ Background(lipgloss.Color("12")).
+ Foreground(lipgloss.Color("0"))
+ line = selectedStyle.Render("> " + line)
+ } else {
+ line = " " + line
+ }
+ content.WriteString(line + "\n")
+ }
+
+ // Fill remaining lines to maintain consistent height
+ remainingLines := MaxVisibleFiles - (visibleEnd - visibleStart)
+ for range remainingLines {
+ content.WriteString("\n")
+ }
+ }
+
+ // Footer with navigation help
+ helpStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("8"))
+ help := helpStyle.Render("↑/↓: navigate, Enter: select/open, Esc: cancel")
+ content.WriteString("\n" + help)
+
+ return containerStyle.Render(content.String())
+}
+
+func min(a, b int) int {
+ if a < b {
+ return a
+ }
+ return b
+}
+
+func (m FilePickerModel) SelectedFile() string {
+ return m.selectedFile
+}