1package tui
2
3import (
4 "fmt"
5 "io/fs"
6 "os"
7 "path/filepath"
8 "strings"
9
10 tea "charm.land/bubbletea/v2"
11 "charm.land/lipgloss/v2"
12)
13
14var (
15 filePickerItemStyle = lipgloss.NewStyle().PaddingLeft(2)
16 filePickerSelectedItemStyle = lipgloss.NewStyle().PaddingLeft(2).Foreground(lipgloss.Color("42"))
17 directoryStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("34"))
18)
19
20type FilePicker struct {
21 cursor int
22 currentPath string
23 items []fs.DirEntry
24 width int
25 height int
26}
27
28func NewFilePicker(startPath string) *FilePicker {
29 fp := &FilePicker{currentPath: startPath}
30 fp.readDir()
31 return fp
32}
33
34func (m *FilePicker) readDir() {
35 files, err := os.ReadDir(m.currentPath)
36 if err != nil {
37 // Handle error, maybe show a message
38 m.items = []fs.DirEntry{}
39 return
40 }
41 m.items = files
42 m.cursor = 0 // Reset cursor
43}
44
45func (m *FilePicker) Init() tea.Cmd {
46 return nil
47}
48
49func (m *FilePicker) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
50 switch msg := msg.(type) {
51 case tea.WindowSizeMsg:
52 m.width = msg.Width
53 m.height = msg.Height
54
55 case tea.KeyPressMsg:
56 switch msg.String() {
57 case "up", "k":
58 if m.cursor > 0 {
59 m.cursor--
60 }
61 case "down", "j":
62 if m.cursor < len(m.items)-1 {
63 m.cursor++
64 }
65 case "enter":
66 if len(m.items) == 0 {
67 return m, nil
68 }
69 selectedItem := m.items[m.cursor]
70 newPath := filepath.Join(m.currentPath, selectedItem.Name())
71
72 if selectedItem.IsDir() {
73 m.currentPath = newPath
74 m.readDir()
75 } else {
76 // It's a file, send a message with the path
77 return m, func() tea.Msg {
78 return FileSelectedMsg{Path: newPath}
79 }
80 }
81 case "backspace":
82 // Go up one directory
83 parentDir := filepath.Dir(m.currentPath)
84 if parentDir != m.currentPath { // Avoid getting stuck at root
85 m.currentPath = parentDir
86 m.readDir()
87 }
88 case "esc", "q":
89 return m, func() tea.Msg { return CancelFilePickerMsg{} }
90 }
91 }
92 return m, nil
93}
94
95func (m *FilePicker) View() tea.View {
96 var b strings.Builder
97
98 b.WriteString(titleStyle.Render("Select a File") + "\n")
99 b.WriteString(fmt.Sprintf("Current Path: %s\n\n", m.currentPath))
100
101 for i, item := range m.items {
102 cursor := " "
103 if m.cursor == i {
104 cursor = "> "
105 }
106
107 itemName := item.Name()
108 if item.IsDir() {
109 itemName = directoryStyle.Render(itemName + "/")
110 }
111
112 line := fmt.Sprintf("%s%s", cursor, itemName)
113
114 if m.cursor == i {
115 b.WriteString(filePickerSelectedItemStyle.Render(line))
116 } else {
117 b.WriteString(filePickerItemStyle.Render(line))
118 }
119 b.WriteString("\n")
120 }
121
122 b.WriteString("\n" + helpStyle.Render("↑/↓: navigate • enter: select • backspace: up • esc: cancel"))
123
124 return tea.NewView(docStyle.Render(b.String()))
125}