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}