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