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}