@@ -1,468 +0,0 @@
1−package dialog
2−
3−// import (
4−// "fmt"
5−// "net/http"
6−// "os"
7−// "path/filepath"
8−// "sort"
9−// "strings"
10−// "time"
11−//
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"
23−// )
24−//
25−// const (
26−// maxAttachmentSize = int64(5 * 1024 * 1024) // 5MB
27−// downArrow = "down"
28−// upArrow = "up"
29−// )
30−//
31−// type FilePrickerKeyMap struct {
32−// Enter key.Binding
33−// Down key.Binding
34−// Up key.Binding
35−// Forward key.Binding
36−// Backward key.Binding
37−// OpenFilePicker key.Binding
38−// Esc key.Binding
39−// InsertCWD key.Binding
40−// }
41−//
42−// var filePickerKeyMap = FilePrickerKeyMap{
43−// Enter: key.NewBinding(
44−// key.WithKeys("enter"),
45−// key.WithHelp("enter", "select file/enter directory"),
46−// ),
47−// Down: key.NewBinding(
48−// key.WithKeys("j", downArrow),
49−// key.WithHelp("↓/j", "down"),
50−// ),
51−// Up: key.NewBinding(
52−// key.WithKeys("k", upArrow),
53−// key.WithHelp("↑/k", "up"),
54−// ),
55−// Forward: key.NewBinding(
56−// key.WithKeys("l"),
57−// key.WithHelp("l", "enter directory"),
58−// ),
59−// Backward: key.NewBinding(
60−// key.WithKeys("h", "backspace"),
61−// key.WithHelp("h/backspace", "go back"),
62−// ),
63−// OpenFilePicker: key.NewBinding(
64−// key.WithKeys("ctrl+f"),
65−// key.WithHelp("ctrl+f", "open file picker"),
66−// ),
67−// Esc: key.NewBinding(
68−// key.WithKeys("esc"),
69−// key.WithHelp("esc", "close/exit"),
70−// ),
71−// InsertCWD: key.NewBinding(
72−// key.WithKeys("i"),
73−// key.WithHelp("i", "manual path input"),
74−// ),
75−// }
76−//
77−// type filepickerCmp struct {
78−// basePath string
79−// width int
80−// height int
81−// cursor int
82−// err error
83−// cursorChain stack
84−// viewport viewport.Model
85−// dirs []os.DirEntry
86−// cwdDetails *DirNode
87−// selectedFile string
88−// cwd textinput.Model
89−// ShowFilePicker bool
90−// app *app.App
91−// }
92−//
93−// type DirNode struct {
94−// parent *DirNode
95−// child *DirNode
96−// directory string
97−// }
98−// type stack []int
99−//
100−// func (s stack) Push(v int) stack {
101−// return append(s, v)
102−// }
103−//
104−// func (s stack) Pop() (stack, int) {
105−// l := len(s)
106−// return s[:l-1], s[l-1]
107−// }
108−//
109−// type AttachmentAddedMsg struct {
110−// Attachment message.Attachment
111−// }
112−//
113−// func (f *filepickerCmp) Init() tea.Cmd {
114−// return nil
115−// }
116−//
117−// func (f *filepickerCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
118−// var cmd tea.Cmd
119−// switch msg := msg.(type) {
120−// case tea.WindowSizeMsg:
121−// f.width = 60
122−// f.height = 20
123−// f.viewport.SetWidth(80)
124−// f.viewport.SetHeight(22)
125−// f.cursor = 0
126−// f.getCurrentFileBelowCursor()
127−// case tea.KeyPressMsg:
128−// if f.cwd.Focused() {
129−// f.cwd, cmd = f.cwd.Update(msg)
130−// }
131−// switch {
132−// case key.Matches(msg, filePickerKeyMap.InsertCWD):
133−// f.cwd.Focus()
134−// return f, cmd
135−// case key.Matches(msg, filePickerKeyMap.Esc):
136−// if f.cwd.Focused() {
137−// f.cwd.Blur()
138−// }
139−// case key.Matches(msg, filePickerKeyMap.Down):
140−// if !f.cwd.Focused() || msg.String() == downArrow {
141−// if f.cursor < len(f.dirs)-1 {
142−// f.cursor++
143−// f.getCurrentFileBelowCursor()
144−// }
145−// }
146−// case key.Matches(msg, filePickerKeyMap.Up):
147−// if !f.cwd.Focused() || msg.String() == upArrow {
148−// if f.cursor > 0 {
149−// f.cursor--
150−// f.getCurrentFileBelowCursor()
151−// }
152−// }
153−// case key.Matches(msg, filePickerKeyMap.Enter):
154−// var path string
155−// var isPathDir bool
156−// if f.cwd.Focused() {
157−// path = f.cwd.Value()
158−// fileInfo, err := os.Stat(path)
159−// if err != nil {
160−// logging.ErrorPersist("Invalid path")
161−// return f, cmd
162−// }
163−// isPathDir = fileInfo.IsDir()
164−// } else {
165−// path = filepath.Join(f.cwdDetails.directory, "/", f.dirs[f.cursor].Name())
166−// isPathDir = f.dirs[f.cursor].IsDir()
167−// }
168−// if isPathDir {
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)
174−// f.cursor = 0
175−// f.cwd.SetValue(f.cwdDetails.directory)
176−// f.getCurrentFileBelowCursor()
177−// } else {
178−// f.selectedFile = path
179−// return f.addAttachmentToMessage()
180−// }
181−// case key.Matches(msg, filePickerKeyMap.Esc):
182−// if !f.cwd.Focused() {
183−// f.cursorChain = make(stack, 0)
184−// f.cursor = 0
185−// } else {
186−// f.cwd.Blur()
187−// }
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)
197−// f.cursor = 0
198−// f.cwd.SetValue(f.cwdDetails.directory)
199−// f.getCurrentFileBelowCursor()
200−// }
201−// }
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()
211−// }
212−// }
213−// case key.Matches(msg, filePickerKeyMap.OpenFilePicker):
214−// f.dirs = readDir(f.cwdDetails.directory, false)
215−// f.cursor = 0
216−// f.getCurrentFileBelowCursor()
217−// }
218−// }
219−// return f, cmd
220−// }
221−//
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))
226−// // return f, nil
227−// // }
228−//
229−// selectedFilePath := f.selectedFile
230−// if !isExtSupported(selectedFilePath) {
231−// logging.ErrorPersist("Unsupported file")
232−// return f, nil
233−// }
234−//
235−// isFileLarge, err := image.ValidateFileSize(selectedFilePath, maxAttachmentSize)
236−// if err != nil {
237−// logging.ErrorPersist("unable to read the image")
238−// return f, nil
239−// }
240−// if isFileLarge {
241−// logging.ErrorPersist("file too large, max 5MB")
242−// return f, nil
243−// }
244−//
245−// content, err := os.ReadFile(selectedFilePath)
246−// if err != nil {
247−// logging.ErrorPersist("Unable read selected file")
248−// return f, nil
249−// }
250−//
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})
257−// }
258−//
259−// func (f *filepickerCmp) View() tea.View {
260−// t := styles.CurrentTheme()
261−// baseStyle := t.S().Base
262−// const maxVisibleDirs = 20
263−// const maxWidth = 80
264−//
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
269−// }
270−// }
271−// adjustedWidth = max(30, min(adjustedWidth, f.width-15)) + 1
272−//
273−// files := make([]string, 0, maxVisibleDirs)
274−// startIdx := 0
275−//
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
282−// }
283−// }
284−//
285−// endIdx := min(startIdx+maxVisibleDirs, len(f.dirs))
286−//
287−// for i := startIdx; i < endIdx; i++ {
288−// file := f.dirs[i]
289−// itemStyle := t.S().Text.Width(adjustedWidth)
290−//
291−// if i == f.cursor {
292−// itemStyle = itemStyle.
293−// Background(t.Primary).
294−// Bold(true)
295−// }
296−// filename := file.Name()
297−//
298−// if len(filename) > adjustedWidth-4 {
299−// filename = filename[:adjustedWidth-7] + "..."
300−// }
301−// if file.IsDir() {
302−// filename = filename + "/"
303−// }
304−// // No need to reassign filename if it's not changing
305−//
306−// files = append(files, itemStyle.Padding(0, 1).Render(filename))
307−// }
308−//
309−// // Pad to always show exactly 21 lines
310−// for len(files) < maxVisibleDirs {
311−// files = append(files, baseStyle.Width(adjustedWidth).Render(""))
312−// }
313−//
314−// currentPath := baseStyle.
315−// Height(1).
316−// Width(adjustedWidth).
317−// Render(f.cwd.View())
318−//
319−// viewportstyle := baseStyle.
320−// Width(f.viewport.Width()).
321−// Border(lipgloss.RoundedBorder()).
322−// BorderForeground(t.BorderFocus).
323−// Padding(2).
324−// Render(f.viewport.View())
325−// var insertExitText string
326−// if f.IsCWDFocused() {
327−// insertExitText = "Press esc to exit typing path"
328−// } else {
329−// insertExitText = "Press i to start typing path"
330−// }
331−//
332−// content := lipgloss.JoinVertical(
333−// lipgloss.Left,
334−// currentPath,
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),
339−// )
340−//
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)
346−//
347−// return tea.NewView(
348−// lipgloss.JoinHorizontal(lipgloss.Center, contentStyle.Render(content), viewportstyle),
349−// )
350−// }
351−//
352−// type FilepickerCmp interface {
353−// util.Model
354−// ToggleFilepicker(showFilepicker bool)
355−// IsCWDFocused() bool
356−// }
357−//
358−// func (f *filepickerCmp) ToggleFilepicker(showFilepicker bool) {
359−// f.ShowFilePicker = showFilepicker
360−// }
361−//
362−// func (f *filepickerCmp) IsCWDFocused() bool {
363−// return f.cwd.Focused()
364−// }
365−//
366−// func NewFilepickerCmp(app *app.App) FilepickerCmp {
367−// homepath, err := os.UserHomeDir()
368−// if err != nil {
369−// logging.Error("error loading user files")
370−// return nil
371−// }
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}
381−// }
382−//
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")
387−// return
388−// }
389−//
390−// dir := f.dirs[f.cursor]
391−// filename := dir.Name()
392−// if !dir.IsDir() && isExtSupported(filename) {
393−// fullPath := f.cwdDetails.directory + "/" + dir.Name()
394−//
395−// go func() {
396−// imageString, err := image.ImagePreview(f.viewport.Width()-4, fullPath)
397−// if err != nil {
398−// logging.Error(err.Error())
399−// f.viewport.SetContent("Preview unavailable")
400−// return
401−// }
402−//
403−// f.viewport.SetContent(imageString)
404−// }()
405−// } else {
406−// f.viewport.SetContent("Preview unavailable")
407−// }
408−// }
409−//
410−// func readDir(path string, showHidden bool) []os.DirEntry {
411−// logging.Info(fmt.Sprintf("Reading directory: %s", path))
412−//
413−// entriesChan := make(chan []os.DirEntry, 1)
414−// errChan := make(chan error, 1)
415−//
416−// go func() {
417−// dirEntries, err := os.ReadDir(path)
418−// if err != nil {
419−// logging.ErrorPersist(err.Error())
420−// errChan <- err
421−// return
422−// }
423−// entriesChan <- dirEntries
424−// }()
425−//
426−// select {
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()
431−// }
432−// return dirEntries[i].IsDir()
433−// })
434−//
435−// if showHidden {
436−// return dirEntries
437−// }
438−//
439−// var sanitizedDirEntries []os.DirEntry
440−// for _, dirEntry := range dirEntries {
441−// isHidden, _ := IsHidden(dirEntry.Name())
442−// if !isHidden {
443−// if dirEntry.IsDir() || isExtSupported(dirEntry.Name()) {
444−// sanitizedDirEntries = append(sanitizedDirEntries, dirEntry)
445−// }
446−// }
447−// }
448−//
449−// return sanitizedDirEntries
450−//
451−// case err := <-errChan:
452−// logging.ErrorPersist(fmt.Sprintf("Error reading directory %s", path), err)
453−// return []os.DirEntry{}
454−//
455−// case <-time.After(5 * time.Second):
456−// logging.ErrorPersist(fmt.Sprintf("Timeout reading directory %s", path), nil)
457−// return []os.DirEntry{}
458−// }
459−// }
460−//
461−// func IsHidden(file string) (bool, error) {
462−// return strings.HasPrefix(file, "."), nil
463−// }
464−//
465−// func isExtSupported(path string) bool {
466−// ext := strings.ToLower(filepath.Ext(path))
467−// return (ext == ".jpg" || ext == ".jpeg" || ext == ".webp" || ext == ".png")
468−// }