@@ -1,468 +0,0 @@
12−// "github.com/charmbracelet/bubbles/v2/key"
13−// "github.com/charmbracelet/bubbles/v2/textinput"
14−// "github.com/charmbracelet/bubbles/v2/viewport"
15−// tea "github.com/charmbracelet/bubbletea/v2"
16−// "github.com/charmbracelet/lipgloss/v2"
17−// "github.com/opencode-ai/opencode/internal/app"
18−// "github.com/opencode-ai/opencode/internal/logging"
19−// "github.com/opencode-ai/opencode/internal/message"
20−// "github.com/opencode-ai/opencode/internal/tui/image"
21−// "github.com/opencode-ai/opencode/internal/tui/styles"
22−// "github.com/opencode-ai/opencode/internal/tui/util"
26−// maxAttachmentSize = int64(5 * 1024 * 1024) // 5MB
31−// type FilePrickerKeyMap struct {
35−// Forward key.Binding
36−// Backward key.Binding
37−// OpenFilePicker key.Binding
39−// InsertCWD key.Binding
42−// var filePickerKeyMap = FilePrickerKeyMap{
43−// Enter: key.NewBinding(
44−// key.WithKeys("enter"),
45−// key.WithHelp("enter", "select file/enter directory"),
47−// Down: key.NewBinding(
48−// key.WithKeys("j", downArrow),
49−// key.WithHelp("↓/j", "down"),
51−// Up: key.NewBinding(
52−// key.WithKeys("k", upArrow),
53−// key.WithHelp("↑/k", "up"),
55−// Forward: key.NewBinding(
57−// key.WithHelp("l", "enter directory"),
59−// Backward: key.NewBinding(
60−// key.WithKeys("h", "backspace"),
61−// key.WithHelp("h/backspace", "go back"),
63−// OpenFilePicker: key.NewBinding(
64−// key.WithKeys("ctrl+f"),
65−// key.WithHelp("ctrl+f", "open file picker"),
67−// Esc: key.NewBinding(
68−// key.WithKeys("esc"),
69−// key.WithHelp("esc", "close/exit"),
71−// InsertCWD: key.NewBinding(
73−// key.WithHelp("i", "manual path input"),
77−// type filepickerCmp struct {
84−// viewport viewport.Model
86−// cwdDetails *DirNode
87−// selectedFile string
88−// cwd textinput.Model
89−// ShowFilePicker bool
93−// type DirNode struct {
100−// func (s stack) Push(v int) stack {
101−// return append(s, v)
104−// func (s stack) Pop() (stack, int) {
106−// return s[:l-1], s[l-1]
109−// type AttachmentAddedMsg struct {
110−// Attachment message.Attachment
113−// func (f *filepickerCmp) Init() tea.Cmd {
117−// func (f *filepickerCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
119−// switch msg := msg.(type) {
120−// case tea.WindowSizeMsg:
123−// f.viewport.SetWidth(80)
124−// f.viewport.SetHeight(22)
126−// f.getCurrentFileBelowCursor()
127−// case tea.KeyPressMsg:
128−// if f.cwd.Focused() {
129−// f.cwd, cmd = f.cwd.Update(msg)
132−// case key.Matches(msg, filePickerKeyMap.InsertCWD):
135−// case key.Matches(msg, filePickerKeyMap.Esc):
136−// if f.cwd.Focused() {
139−// case key.Matches(msg, filePickerKeyMap.Down):
140−// if !f.cwd.Focused() || msg.String() == downArrow {
141−// if f.cursor < len(f.dirs)-1 {
143−// f.getCurrentFileBelowCursor()
146−// case key.Matches(msg, filePickerKeyMap.Up):
147−// if !f.cwd.Focused() || msg.String() == upArrow {
150−// f.getCurrentFileBelowCursor()
153−// case key.Matches(msg, filePickerKeyMap.Enter):
155−// var isPathDir bool
156−// if f.cwd.Focused() {
157−// path = f.cwd.Value()
158−// fileInfo, err := os.Stat(path)
160−// logging.ErrorPersist("Invalid path")
163−// isPathDir = fileInfo.IsDir()
165−// path = filepath.Join(f.cwdDetails.directory, "/", f.dirs[f.cursor].Name())
166−// isPathDir = f.dirs[f.cursor].IsDir()
169−// newWorkingDir := DirNode{parent: f.cwdDetails, directory: path}
170−// f.cwdDetails.child = &newWorkingDir
171−// f.cwdDetails = f.cwdDetails.child
172−// f.cursorChain = f.cursorChain.Push(f.cursor)
173−// f.dirs = readDir(f.cwdDetails.directory, false)
175−// f.cwd.SetValue(f.cwdDetails.directory)
176−// f.getCurrentFileBelowCursor()
178−// f.selectedFile = path
179−// return f.addAttachmentToMessage()
181−// case key.Matches(msg, filePickerKeyMap.Esc):
182−// if !f.cwd.Focused() {
183−// f.cursorChain = make(stack, 0)
188−// case key.Matches(msg, filePickerKeyMap.Forward):
189−// if !f.cwd.Focused() {
190−// if f.dirs[f.cursor].IsDir() {
191−// path := filepath.Join(f.cwdDetails.directory, "/", f.dirs[f.cursor].Name())
192−// newWorkingDir := DirNode{parent: f.cwdDetails, directory: path}
193−// f.cwdDetails.child = &newWorkingDir
194−// f.cwdDetails = f.cwdDetails.child
195−// f.cursorChain = f.cursorChain.Push(f.cursor)
196−// f.dirs = readDir(f.cwdDetails.directory, false)
198−// f.cwd.SetValue(f.cwdDetails.directory)
199−// f.getCurrentFileBelowCursor()
202−// case key.Matches(msg, filePickerKeyMap.Backward):
203−// if !f.cwd.Focused() {
204−// if len(f.cursorChain) != 0 && f.cwdDetails.parent != nil {
205−// f.cursorChain, f.cursor = f.cursorChain.Pop()
206−// f.cwdDetails = f.cwdDetails.parent
207−// f.cwdDetails.child = nil
208−// f.dirs = readDir(f.cwdDetails.directory, false)
209−// f.cwd.SetValue(f.cwdDetails.directory)
210−// f.getCurrentFileBelowCursor()
213−// case key.Matches(msg, filePickerKeyMap.OpenFilePicker):
214−// f.dirs = readDir(f.cwdDetails.directory, false)
216−// f.getCurrentFileBelowCursor()
222−// func (f *filepickerCmp) addAttachmentToMessage() (tea.Model, tea.Cmd) {
223−// // modeInfo := GetSelectedModel(config.Get())
224−// // if !modeInfo.SupportsAttachments {
225−// // logging.ErrorPersist(fmt.Sprintf("Model %s doesn't support attachments", modeInfo.Name))
229−// selectedFilePath := f.selectedFile
230−// if !isExtSupported(selectedFilePath) {
231−// logging.ErrorPersist("Unsupported file")
235−// isFileLarge, err := image.ValidateFileSize(selectedFilePath, maxAttachmentSize)
237−// logging.ErrorPersist("unable to read the image")
241−// logging.ErrorPersist("file too large, max 5MB")
245−// content, err := os.ReadFile(selectedFilePath)
247−// logging.ErrorPersist("Unable read selected file")
251−// mimeBufferSize := min(512, len(content))
252−// mimeType := http.DetectContentType(content[:mimeBufferSize])
253−// fileName := filepath.Base(selectedFilePath)
254−// attachment := message.Attachment{FilePath: selectedFilePath, FileName: fileName, MimeType: mimeType, Content: content}
255−// f.selectedFile = ""
256−// return f, util.CmdHandler(AttachmentAddedMsg{attachment})
259−// func (f *filepickerCmp) View() tea.View {
260−// t := styles.CurrentTheme()
261−// baseStyle := t.S().Base
262−// const maxVisibleDirs = 20
263−// const maxWidth = 80
265−// adjustedWidth := maxWidth
266−// for _, file := range f.dirs {
267−// if len(file.Name()) > adjustedWidth-4 { // Account for padding
268−// adjustedWidth = len(file.Name()) + 4
271−// adjustedWidth = max(30, min(adjustedWidth, f.width-15)) + 1
273−// files := make([]string, 0, maxVisibleDirs)
276−// if len(f.dirs) > maxVisibleDirs {
277−// halfVisible := maxVisibleDirs / 2
278−// if f.cursor >= halfVisible && f.cursor < len(f.dirs)-halfVisible {
279−// startIdx = f.cursor - halfVisible
280−// } else if f.cursor >= len(f.dirs)-halfVisible {
281−// startIdx = len(f.dirs) - maxVisibleDirs
285−// endIdx := min(startIdx+maxVisibleDirs, len(f.dirs))
287−// for i := startIdx; i < endIdx; i++ {
289−// itemStyle := t.S().Text.Width(adjustedWidth)
291−// if i == f.cursor {
292−// itemStyle = itemStyle.
293−// Background(t.Primary).
296−// filename := file.Name()
298−// if len(filename) > adjustedWidth-4 {
299−// filename = filename[:adjustedWidth-7] + "..."
302−// filename = filename + "/"
304−// // No need to reassign filename if it's not changing
306−// files = append(files, itemStyle.Padding(0, 1).Render(filename))
309−// // Pad to always show exactly 21 lines
310−// for len(files) < maxVisibleDirs {
311−// files = append(files, baseStyle.Width(adjustedWidth).Render(""))
314−// currentPath := baseStyle.
316−// Width(adjustedWidth).
317−// Render(f.cwd.View())
319−// viewportstyle := baseStyle.
320−// Width(f.viewport.Width()).
321−// Border(lipgloss.RoundedBorder()).
322−// BorderForeground(t.BorderFocus).
324−// Render(f.viewport.View())
325−// var insertExitText string
326−// if f.IsCWDFocused() {
327−// insertExitText = "Press esc to exit typing path"
329−// insertExitText = "Press i to start typing path"
332−// content := lipgloss.JoinVertical(
335−// baseStyle.Width(adjustedWidth).Render(""),
336−// baseStyle.Width(adjustedWidth).Render(lipgloss.JoinVertical(lipgloss.Left, files...)),
337−// baseStyle.Width(adjustedWidth).Render(""),
338−// t.S().Muted.Width(adjustedWidth).Render(insertExitText),
341−// f.cwd.SetValue(f.cwd.Value())
342−// contentStyle := baseStyle.Padding(1, 2).
343−// Border(lipgloss.RoundedBorder()).
344−// BorderForeground(t.BorderFocus).
345−// Width(lipgloss.Width(content) + 4)
347−// return tea.NewView(
348−// lipgloss.JoinHorizontal(lipgloss.Center, contentStyle.Render(content), viewportstyle),
352−// type FilepickerCmp interface {
354−// ToggleFilepicker(showFilepicker bool)
355−// IsCWDFocused() bool
358−// func (f *filepickerCmp) ToggleFilepicker(showFilepicker bool) {
359−// f.ShowFilePicker = showFilepicker
362−// func (f *filepickerCmp) IsCWDFocused() bool {
363−// return f.cwd.Focused()
366−// func NewFilepickerCmp(app *app.App) FilepickerCmp {
367−// homepath, err := os.UserHomeDir()
369−// logging.Error("error loading user files")
372−// baseDir := DirNode{parent: nil, directory: homepath}
373−// dirs := readDir(homepath, false)
374−// viewport := viewport.New()
375−// currentDirectory := textinput.New()
376−// currentDirectory.CharLimit = 200
377−// currentDirectory.SetWidth(44)
378−// currentDirectory.Cursor().Blink = true
379−// currentDirectory.SetValue(baseDir.directory)
380−// return &filepickerCmp{cwdDetails: &baseDir, dirs: dirs, cursorChain: make(stack, 0), viewport: viewport, cwd: currentDirectory, app: app}
383−// func (f *filepickerCmp) getCurrentFileBelowCursor() {
384−// if len(f.dirs) == 0 || f.cursor < 0 || f.cursor >= len(f.dirs) {
385−// logging.Error(fmt.Sprintf("Invalid cursor position. Dirs length: %d, Cursor: %d", len(f.dirs), f.cursor))
386−// f.viewport.SetContent("Preview unavailable")
390−// dir := f.dirs[f.cursor]
391−// filename := dir.Name()
392−// if !dir.IsDir() && isExtSupported(filename) {
393−// fullPath := f.cwdDetails.directory + "/" + dir.Name()
396−// imageString, err := image.ImagePreview(f.viewport.Width()-4, fullPath)
398−// logging.Error(err.Error())
399−// f.viewport.SetContent("Preview unavailable")
403−// f.viewport.SetContent(imageString)
406−// f.viewport.SetContent("Preview unavailable")
410−// func readDir(path string, showHidden bool) []os.DirEntry {
411−// logging.Info(fmt.Sprintf("Reading directory: %s", path))
413−// entriesChan := make(chan []os.DirEntry, 1)
414−// errChan := make(chan error, 1)
417−// dirEntries, err := os.ReadDir(path)
419−// logging.ErrorPersist(err.Error())
423−// entriesChan <- dirEntries
427−// case dirEntries := <-entriesChan:
428−// sort.Slice(dirEntries, func(i, j int) bool {
429−// if dirEntries[i].IsDir() == dirEntries[j].IsDir() {
430−// return dirEntries[i].Name() < dirEntries[j].Name()
432−// return dirEntries[i].IsDir()
439−// var sanitizedDirEntries []os.DirEntry
440−// for _, dirEntry := range dirEntries {
441−// isHidden, _ := IsHidden(dirEntry.Name())
443−// if dirEntry.IsDir() || isExtSupported(dirEntry.Name()) {
444−// sanitizedDirEntries = append(sanitizedDirEntries, dirEntry)
449−// return sanitizedDirEntries
451−// case err := <-errChan:
452−// logging.ErrorPersist(fmt.Sprintf("Error reading directory %s", path), err)
453−// return []os.DirEntry{}
455−// case <-time.After(5 * time.Second):
456−// logging.ErrorPersist(fmt.Sprintf("Timeout reading directory %s", path), nil)
457−// return []os.DirEntry{}
461−// func IsHidden(file string) (bool, error) {
462−// return strings.HasPrefix(file, "."), nil
465−// func isExtSupported(path string) bool {
466−// ext := strings.ToLower(filepath.Ext(path))
467−// return (ext == ".jpg" || ext == ".jpeg" || ext == ".webp" || ext == ".png")