filepicker.go

  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}