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