110 lines
2.5 KiB
Go
110 lines
2.5 KiB
Go
package tui
|
|
|
|
import (
|
|
"errors"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.jingrow.com/charmbracelet/bubbles/filepicker"
|
|
tea "git.jingrow.com/charmbracelet/bubbletea"
|
|
"git.jingrow.com/charmbracelet/lipgloss"
|
|
)
|
|
|
|
var BUBBLE_TEA_FILEPICKER_STYLE filepicker.Styles = filepicker.DefaultStylesWithRenderer(lipgloss.DefaultRenderer())
|
|
|
|
func PickFile(prompt string, allowedExtensions []string) (selectedFile string, err error) {
|
|
fp := filepicker.New()
|
|
fp.AllowedTypes = allowedExtensions
|
|
fp.CurrentDirectory, _ = os.UserHomeDir()
|
|
fp.Styles = BUBBLE_TEA_FILEPICKER_STYLE
|
|
|
|
m := &pickerModel{
|
|
filepicker: fp,
|
|
prompt: prompt,
|
|
}
|
|
|
|
p := tea.NewProgram(m)
|
|
if _, err := p.Run(); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
// If we have a selected file, return it (regardless of quit status)
|
|
if m.selectedFile != "" {
|
|
return m.selectedFile, nil
|
|
}
|
|
|
|
// No file selected - return empty string
|
|
return "", nil
|
|
}
|
|
|
|
// Internal model implementation
|
|
type pickerModel struct {
|
|
filepicker filepicker.Model
|
|
selectedFile string
|
|
quitting bool
|
|
err error
|
|
prompt string
|
|
}
|
|
|
|
type clearErrorMsg struct{}
|
|
|
|
func clearErrorAfter(t time.Duration) tea.Cmd {
|
|
return tea.Tick(t, func(_ time.Time) tea.Msg {
|
|
return clearErrorMsg{}
|
|
})
|
|
}
|
|
|
|
func (m *pickerModel) Init() tea.Cmd {
|
|
return m.filepicker.Init()
|
|
}
|
|
|
|
func (m *pickerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
switch msg := msg.(type) {
|
|
case tea.KeyMsg:
|
|
switch msg.String() {
|
|
case "ctrl+q":
|
|
m.quitting = true
|
|
return m, tea.Quit
|
|
}
|
|
case clearErrorMsg:
|
|
m.err = nil
|
|
}
|
|
|
|
var cmd tea.Cmd
|
|
m.filepicker, cmd = m.filepicker.Update(msg)
|
|
|
|
if didSelect, path := m.filepicker.DidSelectFile(msg); didSelect {
|
|
m.selectedFile = path
|
|
m.quitting = true
|
|
return m, tea.Quit
|
|
}
|
|
|
|
if didSelect, path := m.filepicker.DidSelectDisabledFile(msg); didSelect {
|
|
m.err = errors.New(path + " is not valid")
|
|
m.selectedFile = ""
|
|
return m, tea.Batch(cmd, clearErrorAfter(2*time.Second))
|
|
}
|
|
|
|
return m, cmd
|
|
}
|
|
|
|
func (m *pickerModel) View() string {
|
|
if m.quitting {
|
|
return ""
|
|
}
|
|
var s strings.Builder
|
|
s.WriteString("\n ")
|
|
if m.err != nil {
|
|
s.WriteString(titleStyle.Render(m.filepicker.Styles.DisabledFile.Render(m.err.Error())))
|
|
} else if m.selectedFile == "" {
|
|
s.WriteString(titleStyle.Render(m.prompt))
|
|
} else {
|
|
s.WriteString("Selected file: " + m.filepicker.Styles.Selected.Render(m.selectedFile))
|
|
}
|
|
s.WriteString("\n\n" + m.filepicker.View() + "\n")
|
|
s.WriteString(" <arrow> to navigate • <enter> to select • <ctrl+q> to quit\n")
|
|
|
|
return s.String()
|
|
}
|