filepicker.go

  1package filepicker
  2
  3import (
  4	"os"
  5	"strings"
  6
  7	"github.com/charmbracelet/bubbles/v2/filepicker"
  8	tea "github.com/charmbracelet/bubbletea/v2"
  9	"github.com/charmbracelet/lipgloss/v2"
 10	"github.com/opencode-ai/opencode/internal/tui/components/core"
 11	"github.com/opencode-ai/opencode/internal/tui/components/dialogs"
 12	"github.com/opencode-ai/opencode/internal/tui/styles"
 13)
 14
 15const (
 16	maxAttachmentSize  = int64(5 * 1024 * 1024) // 5MB
 17	FilePickerID       = "filepicker"
 18	fileSelectionHight = 10
 19)
 20
 21type FilePicker interface {
 22	dialogs.DialogModel
 23}
 24
 25type filePicker struct {
 26	wWidth       int
 27	wHeight      int
 28	width        int
 29	filepicker   filepicker.Model
 30	selectedFile string
 31}
 32
 33func NewFilePickerCmp() FilePicker {
 34	t := styles.CurrentTheme()
 35	fp := filepicker.New()
 36	fp.AllowedTypes = []string{".jpg", ".jpeg", ".png"}
 37	fp.CurrentDirectory, _ = os.UserHomeDir()
 38	fp.ShowPermissions = false
 39	fp.ShowSize = false
 40	fp.AutoHeight = false
 41	fp.Styles = t.S().FilePicker
 42	fp.Cursor = ""
 43	fp.SetHeight(fileSelectionHight)
 44
 45	return &filePicker{filepicker: fp}
 46}
 47
 48func (m *filePicker) Init() tea.Cmd {
 49	return m.filepicker.Init()
 50}
 51
 52func (m *filePicker) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 53	switch msg := msg.(type) {
 54	case tea.WindowSizeMsg:
 55		m.wWidth = msg.Width
 56		m.wHeight = msg.Height
 57		m.width = min(70, m.wWidth)
 58		styles := m.filepicker.Styles
 59		styles.Directory = styles.Directory.Width(m.width - 4)
 60		styles.Selected = styles.Selected.PaddingLeft(1).Width(m.width - 4)
 61		styles.DisabledSelected = styles.DisabledSelected.PaddingLeft(1).Width(m.width - 4)
 62		styles.File = styles.File.Width(m.width)
 63		m.filepicker.Styles = styles
 64		return m, nil
 65	}
 66
 67	var cmd tea.Cmd
 68	m.filepicker, cmd = m.filepicker.Update(msg)
 69
 70	// Did the user select a file?
 71	if didSelect, path := m.filepicker.DidSelectFile(msg); didSelect {
 72		// Get the path of the selected file.
 73		m.selectedFile = path
 74	}
 75
 76	return m, cmd
 77}
 78
 79func (m *filePicker) View() tea.View {
 80	t := styles.CurrentTheme()
 81
 82	content := lipgloss.JoinVertical(
 83		lipgloss.Left,
 84		t.S().Base.Padding(0, 1, 1, 1).Render(core.Title("Add Image", m.width-4)),
 85		m.imagePreview(),
 86		m.filepicker.View(),
 87	)
 88	return tea.NewView(m.style().Render(content))
 89}
 90
 91func (m *filePicker) currentImage() string {
 92	for _, ext := range m.filepicker.AllowedTypes {
 93		if strings.HasSuffix(m.filepicker.HighlightedPath(), ext) {
 94			return m.filepicker.HighlightedPath()
 95		}
 96	}
 97	return ""
 98}
 99
100func (m *filePicker) imagePreview() string {
101	if m.currentImage() == "" {
102		return m.imagePreviewStyle().Render()
103	}
104
105	return ""
106}
107
108func (m *filePicker) imagePreviewStyle() lipgloss.Style {
109	t := styles.CurrentTheme()
110	w, h := m.imagePreviewSize()
111	return t.S().Base.
112		Width(w).
113		Height(h).
114		Margin(1).
115		Background(t.BgOverlay)
116}
117
118func (m *filePicker) imagePreviewSize() (int, int) {
119	return m.width - 4, min(20, m.wHeight/2)
120}
121
122func (m *filePicker) style() lipgloss.Style {
123	t := styles.CurrentTheme()
124	return t.S().Base.
125		Width(m.width).
126		Border(lipgloss.RoundedBorder()).
127		BorderForeground(t.BorderFocus)
128}
129
130// ID implements FilePicker.
131func (m *filePicker) ID() dialogs.DialogID {
132	return FilePickerID
133}
134
135// Position implements FilePicker.
136func (m *filePicker) Position() (int, int) {
137	row := m.wHeight/4 - 2 // just a bit above the center
138	col := m.wWidth / 2
139	col -= m.width / 2
140	return row, col
141}