Skip to content

Commit b5d442b

Browse files
authored
Improve file browser (#138)
* Improve list navigation * Improve file browser
1 parent 7f2e358 commit b5d442b

1 file changed

Lines changed: 62 additions & 21 deletions

File tree

ui/filebrowser.go

Lines changed: 62 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,12 @@ type fbTracksResolvedMsg struct {
3030

3131
// openFileBrowser initialises and shows the file browser overlay.
3232
func (m *Model) openFileBrowser() {
33-
home, err := os.UserHomeDir()
34-
if err != nil {
35-
home = "/"
33+
if m.fileBrowser.dir == "" {
34+
m.fileBrowser.dir, _ = os.UserHomeDir()
35+
if m.fileBrowser.dir == "" {
36+
m.fileBrowser.dir = "/"
37+
}
3638
}
37-
m.fileBrowser.dir = home
3839
m.fileBrowser.cursor = 0
3940
m.fileBrowser.selected = make(map[string]bool)
4041
m.fileBrowser.err = ""
@@ -45,7 +46,14 @@ func (m *Model) openFileBrowser() {
4546
// loadFBDir reads the current directory and populates fbEntries.
4647
func (m *Model) loadFBDir() {
4748
m.fileBrowser.err = ""
48-
m.fileBrowser.entries = nil
49+
m.fileBrowser.cursor = 0
50+
51+
// Reuse internal memory buffer of m.fileBrowser.entries.
52+
m.fileBrowser.entries = m.fileBrowser.entries[:0]
53+
if cap(m.fileBrowser.entries) > 512 {
54+
// Previous directory list was too large, do not retain memory, re-allocate buffer.
55+
m.fileBrowser.entries = nil
56+
}
4957

5058
// Always provide a parent entry for navigating up.
5159
m.fileBrowser.entries = append(m.fileBrowser.entries, fbEntry{
@@ -55,40 +63,55 @@ func (m *Model) loadFBDir() {
5563
isParent: true,
5664
})
5765

66+
// Get entries sorted by name, dirs and files mixed
5867
entries, err := os.ReadDir(m.fileBrowser.dir)
5968
if err != nil {
6069
m.fileBrowser.err = err.Error()
61-
m.fileBrowser.cursor = 0
6270
return
6371
}
6472

65-
// Separate dirs and files, skip dotfiles.
66-
var dirs, files []fbEntry
73+
// Add directories to m.fileBrowser.entries (reuse internal memory),
74+
// add files to files, then append all files to m.fileBrowser.entries, skip dotfiles.
75+
var files []fbEntry
6776
for _, e := range entries {
6877
name := e.Name()
6978
if strings.HasPrefix(name, ".") {
7079
continue
7180
}
72-
full := filepath.Join(m.fileBrowser.dir, name)
81+
// Detect directories and directory-like entries.
82+
dirType := "" // Name suffix for directories and some non-regular file types.
7383
if e.IsDir() {
74-
dirs = append(dirs, fbEntry{
75-
name: name + "/",
76-
path: full,
84+
dirType = "/"
85+
} else if !e.Type().IsRegular() {
86+
if e.Type()&os.ModeSymlink != 0 && !player.SupportedExts[strings.ToLower(filepath.Ext(name))] {
87+
// Treat symlink as a directory unless it points to media file.
88+
// os.DirEntry has no option to test the type of object symlink points to.
89+
dirType = "@"
90+
} else if os.PathSeparator == '\\' && e.Type()&os.ModeIrregular != 0 {
91+
// Try to support directory junctions on Windows (mklink /J).
92+
// Go do not support such files, it treats them as os.ModeIrregular (?---------).
93+
dirType = "?"
94+
}
95+
}
96+
// Add entry to m.fileBrowser.entries or to files slice
97+
if dirType != "" {
98+
m.fileBrowser.entries = append(m.fileBrowser.entries, fbEntry{
99+
name: name + dirType,
100+
path: filepath.Join(m.fileBrowser.dir, name),
77101
isDir: true,
78102
})
79103
} else {
80-
ext := strings.ToLower(filepath.Ext(name))
104+
if files == nil {
105+
files = make([]fbEntry, 0, 16) // Avoid reallocations
106+
}
81107
files = append(files, fbEntry{
82108
name: name,
83-
path: full,
84-
isAudio: player.SupportedExts[ext],
109+
path: filepath.Join(m.fileBrowser.dir, name),
110+
isAudio: player.SupportedExts[strings.ToLower(filepath.Ext(name))],
85111
})
86112
}
87113
}
88-
89-
m.fileBrowser.entries = append(m.fileBrowser.entries, dirs...)
90114
m.fileBrowser.entries = append(m.fileBrowser.entries, files...)
91-
m.fileBrowser.cursor = 0
92115
}
93116

94117
// handleFileBrowserKey processes key presses while the file browser is open.
@@ -98,9 +121,8 @@ func (m *Model) handleFileBrowserKey(msg tea.KeyMsg) tea.Cmd {
98121
m.fileBrowser.visible = false
99122
return m.quit()
100123

101-
case "esc", "o":
124+
case "esc", "o", "q":
102125
m.fileBrowser.visible = false
103-
return nil
104126

105127
case "up", "k":
106128
if m.fileBrowser.cursor > 0 {
@@ -145,6 +167,20 @@ func (m *Model) handleFileBrowserKey(msg tea.KeyMsg) tea.Cmd {
145167
m.fileBrowser.dir = filepath.Dir(m.fileBrowser.dir)
146168
m.loadFBDir()
147169

170+
case "~":
171+
cd, _ := os.UserHomeDir()
172+
if cd != "" && m.fileBrowser.dir != cd {
173+
m.fileBrowser.dir = cd
174+
m.loadFBDir()
175+
}
176+
177+
case ".":
178+
cd, _ := os.Getwd()
179+
if cd != "" && m.fileBrowser.dir != cd {
180+
m.fileBrowser.dir = cd
181+
m.loadFBDir()
182+
}
183+
148184
case " ":
149185
if m.fileBrowser.cursor < len(m.fileBrowser.entries) {
150186
e := m.fileBrowser.entries[m.fileBrowser.cursor]
@@ -155,6 +191,9 @@ func (m *Model) handleFileBrowserKey(msg tea.KeyMsg) tea.Cmd {
155191
m.fileBrowser.selected[e.path] = true
156192
}
157193
}
194+
if m.fileBrowser.cursor < len(m.fileBrowser.entries)-1 {
195+
m.fileBrowser.cursor++
196+
}
158197
}
159198

160199
case "a":
@@ -284,7 +323,9 @@ func (m Model) renderFileBrowser() string {
284323
lines = append(lines, "")
285324
}
286325

287-
help := helpKey("↑↓", "Navigate ") + helpKey("Enter", "Open ") + helpKey("Spc", "Select ") + helpKey("a", "All ") + helpKey("←", "Back ")
326+
help := helpKey("↑↓", "Scroll ") + helpKey("Enter", "Open ") +
327+
helpKey("Spc", "Select ") + helpKey("a", "All ") +
328+
helpKey("←", "Back ") + helpKey("~.", "Home/Cwd ")
288329
if len(m.fileBrowser.selected) > 0 {
289330
help += helpKey("R", "Replace ")
290331
}

0 commit comments

Comments
 (0)