Skip to content

Commit 8242ebf

Browse files
committed
feat: Allow hover to file
1 parent d9ad4db commit 8242ebf

File tree

7 files changed

+124
-59
lines changed

7 files changed

+124
-59
lines changed

src/cmd/main.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -120,9 +120,9 @@ func Run(content embed.FS) {
120120

121121
func spfAppAction(_ context.Context, c *cli.Command) error {
122122
// If no args are called along with "spf" use current dir
123-
firstFilePanelDirs := []string{""}
123+
firstPanelPaths := []string{""}
124124
if c.Args().Present() {
125-
firstFilePanelDirs = c.Args().Slice()
125+
firstPanelPaths = c.Args().Slice()
126126
}
127127

128128
variable.UpdateVarFromCliArgs(c)
@@ -131,7 +131,7 @@ func spfAppAction(_ context.Context, c *cli.Command) error {
131131

132132
firstUse := checkFirstUse()
133133

134-
p := tea.NewProgram(internal.InitialModel(firstFilePanelDirs, firstUse),
134+
p := tea.NewProgram(internal.InitialModel(firstPanelPaths, firstUse),
135135
tea.WithAltScreen(), tea.WithMouseCellMotion())
136136
if _, err := p.Run(); err != nil {
137137
utils.PrintfAndExitf("Alas, there's been an error: %v", err)

src/internal/config_function.go

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import (
2525
// themes) setted up. Processes input directories and returns toggle states.
2626

2727
// This is the only usecase of named returns, distinguish between multiple return values
28-
func initialConfig(firstFilePanelDirs []string) (toggleDotFile bool, //nolint: nonamedreturns // See above
28+
func initialConfig(firstPanelPaths []string) (toggleDotFile bool, //nolint: nonamedreturns // See above
2929
toggleFooter bool, zClient *zoxidelib.Client) {
3030
// Open log stream
3131
file, err := os.OpenFile(variable.LogFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
@@ -80,9 +80,9 @@ func initialConfig(firstFilePanelDirs []string) (toggleDotFile bool, //nolint: n
8080
}
8181
}
8282

83-
updateFirstFilePanelDirs(firstFilePanelDirs, cwd, zClient)
83+
updateFirstFilePanelPaths(firstPanelPaths, cwd, zClient)
8484

85-
slog.Debug("Directory configuration", "cwd", cwd, "start_directories", firstFilePanelDirs)
85+
slog.Debug("Directory configuration", "cwd", cwd, "start_paths", firstPanelPaths)
8686
printRuntimeInfo()
8787

8888
toggleDotFile = utils.ReadBoolFile(variable.ToggleDotFile, false)
@@ -91,27 +91,27 @@ func initialConfig(firstFilePanelDirs []string) (toggleDotFile bool, //nolint: n
9191
return toggleDotFile, toggleFooter, zClient
9292
}
9393

94-
func updateFirstFilePanelDirs(firstFilePanelDirs []string, cwd string, zClient *zoxidelib.Client) {
95-
for i := range firstFilePanelDirs {
96-
if firstFilePanelDirs[i] == "" {
97-
firstFilePanelDirs[i] = common.Config.DefaultDirectory
94+
func updateFirstFilePanelPaths(firstPanelPaths []string, cwd string, zClient *zoxidelib.Client) {
95+
for i := range firstPanelPaths {
96+
if firstPanelPaths[i] == "" {
97+
firstPanelPaths[i] = common.Config.DefaultDirectory
9898
}
99-
originalPath := firstFilePanelDirs[i]
100-
firstFilePanelDirs[i] = utils.ResolveAbsPath(cwd, firstFilePanelDirs[i])
101-
if _, err := os.Stat(firstFilePanelDirs[i]); err != nil {
99+
originalPath := firstPanelPaths[i]
100+
firstPanelPaths[i] = utils.ResolveAbsPath(cwd, firstPanelPaths[i])
101+
if _, err := os.Stat(firstPanelPaths[i]); err != nil {
102102
slog.Error("cannot get stats for firstFilePanelDir", "error", err)
103103
// In case the path provided did not exist, use zoxide query
104104
// else, fallback to home dir
105105
if common.Config.ZoxideSupport && zClient != nil {
106106
path, err := attemptZoxideForInitPath(originalPath, zClient)
107107
if err != nil {
108108
slog.Error("Zoxide query error", "originalPath", originalPath, "error", err)
109-
firstFilePanelDirs[i] = variable.HomeDir
109+
firstPanelPaths[i] = variable.HomeDir
110110
} else {
111-
firstFilePanelDirs[i] = path
111+
firstPanelPaths[i] = path
112112
}
113113
} else {
114-
firstFilePanelDirs[i] = variable.HomeDir
114+
firstPanelPaths[i] = variable.HomeDir
115115
}
116116
}
117117
}

src/internal/default_config.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,15 @@ import (
2020
// TODO: Move the configuration parameters to a ModelConfig struct.
2121
// Something like `RendererConfig` struct for `Renderer` struct in ui/renderer package
2222
func defaultModelConfig(toggleDotFile, toggleFooter, firstUse bool,
23-
firstFilePanelDirs []string, zClient *zoxidelib.Client) *model {
23+
firstPanelPaths []string, zClient *zoxidelib.Client) *model {
2424
return &model{
2525
filePanelFocusIndex: 0,
2626
focusPanel: nonePanelFocus,
2727
processBarModel: processbar.New(),
2828
sidebarModel: sidebar.New(),
2929
fileMetaData: metadata.New(),
3030
fileModel: fileModel{
31-
filePanels: filePanelSlice(firstFilePanelDirs),
31+
filePanels: filePanelSlice(firstPanelPaths),
3232
filePreview: preview.New(),
3333
width: 10,
3434
},

src/internal/model.go

Lines changed: 67 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,9 @@ var (
3838
// is passed to tea.NewProgram() which accepts tea.Model
3939
// Either way type 'model' is not exported, so there is not way main package can
4040
// be aware of it, and use it directly
41-
func InitialModel(firstFilePanelDirs []string, firstUseCheck bool) tea.Model {
42-
toggleDotFile, toggleFooter, zClient := initialConfig(firstFilePanelDirs)
43-
return defaultModelConfig(toggleDotFile, toggleFooter, firstUseCheck, firstFilePanelDirs, zClient)
41+
func InitialModel(firstPanelPaths []string, firstUseCheck bool) tea.Model {
42+
toggleDotFile, toggleFooter, zClient := initialConfig(firstPanelPaths)
43+
return defaultModelConfig(toggleDotFile, toggleFooter, firstUseCheck, firstPanelPaths, zClient)
4444
}
4545

4646
// Init function to be called by Bubble tea framework, sets windows title,
@@ -698,50 +698,82 @@ func getMaxW(s string) int {
698698
func (m *model) getFilePanelItems() {
699699
focusPanel := m.fileModel.filePanels[m.filePanelFocusIndex]
700700
for i, filePanel := range m.fileModel.filePanels {
701-
var fileElement []element
702701
nowTime := time.Now()
703-
// Check last time each element was updated, if less then 3 seconds ignore
704-
if !filePanel.isFocused && nowTime.Sub(filePanel.lastTimeGetElement) < 3*time.Second {
705-
// TODO : revisit this. This feels like a duct tape solution of an actual
706-
// deep rooted problem. This feels very hacky.
707-
if !m.updatedToggleDotFile {
708-
continue
709-
}
710-
}
711-
712-
focusPanelReRender := false
713-
714-
if len(focusPanel.element) > 0 {
715-
if filepath.Dir(focusPanel.element[0].location) != focusPanel.location {
716-
focusPanelReRender = true
717-
}
718-
} else {
719-
focusPanelReRender = true
720-
}
721-
722-
reRenderTime := int(float64(len(filePanel.element)) / 100)
723-
724-
if filePanel.isFocused && !focusPanelReRender &&
725-
nowTime.Sub(filePanel.lastTimeGetElement) < time.Duration(reRenderTime)*time.Second {
702+
if m.shouldSkipPanelUpdate(filePanel, focusPanel, nowTime) {
726703
continue
727704
}
728705

729-
// Get file names based on search bar filter
730-
if filePanel.searchBar.Value() != "" {
731-
fileElement = returnDirElementBySearchString(filePanel.location, m.toggleDotFile,
732-
filePanel.searchBar.Value(), filePanel.sortOptions.data)
733-
} else {
734-
fileElement = returnDirElement(filePanel.location, m.toggleDotFile, filePanel.sortOptions.data)
735-
}
706+
// Load elements for this panel (with/without search filter)
707+
fileElement := m.getElementsForPanel(filePanel)
708+
736709
// Update file panel list
737-
filePanel.element = fileElement
738710
m.fileModel.filePanels[i].element = fileElement
739711
m.fileModel.filePanels[i].lastTimeGetElement = nowTime
712+
713+
// Position cursor on target file when provided
714+
m.applyTargetFileCursor(i, fileElement)
740715
}
741716

742717
m.updatedToggleDotFile = false
743718
}
744719

720+
// Helper to decide whether to skip updating a panel this tick.
721+
func (m *model) shouldSkipPanelUpdate(filePanel filePanel, focusPanel filePanel, nowTime time.Time) bool {
722+
// Throttle non-focused panels unless dotfile toggle changed
723+
if !filePanel.isFocused && nowTime.Sub(filePanel.lastTimeGetElement) < 3*time.Second {
724+
if !m.updatedToggleDotFile {
725+
return true
726+
}
727+
}
728+
729+
focusPanelReRender := m.focusPanelNeedsReRender(focusPanel)
730+
reRenderTime := int(float64(len(filePanel.element)) / 100)
731+
if filePanel.isFocused && !focusPanelReRender &&
732+
nowTime.Sub(filePanel.lastTimeGetElement) < time.Duration(reRenderTime)*time.Second {
733+
return true
734+
}
735+
return false
736+
}
737+
738+
// Checks whether the focus panel directory changed and forces a re-render.
739+
func (m *model) focusPanelNeedsReRender(focusPanel filePanel) bool {
740+
if len(focusPanel.element) > 0 {
741+
return filepath.Dir(focusPanel.element[0].location) != focusPanel.location
742+
}
743+
return true
744+
}
745+
746+
// Retrieves elements for a panel based on search bar value and sort options.
747+
func (m *model) getElementsForPanel(filePanel filePanel) []element {
748+
if filePanel.searchBar.Value() != "" {
749+
return returnDirElementBySearchString(
750+
filePanel.location,
751+
m.toggleDotFile,
752+
filePanel.searchBar.Value(),
753+
filePanel.sortOptions.data,
754+
)
755+
}
756+
return returnDirElement(filePanel.location, m.toggleDotFile, filePanel.sortOptions.data)
757+
}
758+
759+
// Applies targetFile cursor positioning, if configured for the panel.
760+
func (m *model) applyTargetFileCursor(panelIndex int, elements []element) {
761+
if tf := m.fileModel.filePanels[panelIndex].targetFile; tf == "" {
762+
return
763+
} else {
764+
for idx, el := range elements {
765+
if el.name == tf {
766+
m.fileModel.filePanels[panelIndex].cursor = idx
767+
if idx > 0 {
768+
m.fileModel.filePanels[panelIndex].render = idx - 1
769+
}
770+
break
771+
}
772+
}
773+
m.fileModel.filePanels[panelIndex].targetFile = ""
774+
}
775+
}
776+
745777
// Close superfile application. Cd into the current dir if CdOnQuit on and save
746778
// the path in state direcotory
747779
func (m *model) quitSuperfile(cdOnQuit bool) {

src/internal/model_test.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,27 @@ func TestBasic(t *testing.T) {
9595
})
9696
}
9797

98+
func TestInitialFilePathPositionsCursor(t *testing.T) {
99+
curTestDir := t.TempDir()
100+
dir1 := filepath.Join(curTestDir, "dir1")
101+
file1 := filepath.Join(dir1, "file1.txt")
102+
file2 := filepath.Join(dir1, "file2.txt")
103+
file3 := filepath.Join(dir1, "file3.txt")
104+
105+
utils.SetupDirectories(t, curTestDir, dir1)
106+
utils.SetupFiles(t, file1, file2, file3)
107+
108+
// Initialize model with a file path; cursor should land on that file
109+
m := defaultTestModel(dir1, file2)
110+
p := NewTestTeaProgWithEventLoop(t, m)
111+
// Cause an blocking Update()
112+
p.SendDirectly(nil)
113+
// Populate elements for the panel and apply target cursor
114+
require.Len(t, m.fileModel.filePanels, 2)
115+
assert.Equal(t, m.fileModel.filePanels[0].location, dir1)
116+
assert.Equal(t, m.fileModel.filePanels[1].getSelectedItem().location, file2)
117+
}
118+
98119
func TestQuit(t *testing.T) {
99120
// Test
100121
// 1 - Normal quit

src/internal/type.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ type filePanel struct {
167167
renaming bool
168168
searchBar textinput.Model
169169
lastTimeGetElement time.Time
170+
targetFile string // filename to position cursor on after load
170171
}
171172

172173
// Sort options

src/internal/type_utils.go

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package internal
22

33
import (
44
"fmt"
5+
"os"
6+
"path/filepath"
57

68
"github.com/yorukot/superfile/src/internal/common"
79
"github.com/yorukot/superfile/src/internal/utils"
@@ -36,21 +38,29 @@ func (m *model) validateLayout() error {
3638

3739
// ================ filepanel
3840

39-
func filePanelSlice(dir []string) []filePanel {
40-
res := make([]filePanel, len(dir))
41-
for i := range dir {
41+
func filePanelSlice(paths []string) []filePanel {
42+
res := make([]filePanel, len(paths))
43+
for i := range paths {
4244
// Making the first panel as the focussed
4345
isFocus := i == 0
44-
res[i] = defaultFilePanel(dir[i], isFocus)
46+
res[i] = defaultFilePanel(paths[i], isFocus)
4547
}
4648
return res
4749
}
4850

49-
func defaultFilePanel(dir string, focused bool) filePanel {
51+
func defaultFilePanel(path string, focused bool) filePanel {
52+
targetFile := ""
53+
panelPath := path
54+
// If path refers to a file, switch to its parent and remember the filename
55+
if stat, err := os.Stat(panelPath); err == nil && !stat.IsDir() {
56+
targetFile = filepath.Base(panelPath)
57+
panelPath = filepath.Dir(panelPath)
58+
}
59+
5060
return filePanel{
5161
render: 0,
5262
cursor: 0,
53-
location: dir,
63+
location: panelPath,
5464
sortOptions: sortOptionsModel{
5565
width: 20,
5666
height: 4,
@@ -69,6 +79,7 @@ func defaultFilePanel(dir string, focused bool) filePanel {
6979
isFocused: focused,
7080
directoryRecords: make(map[string]directoryRecord),
7181
searchBar: common.GenerateSearchBar(),
82+
targetFile: targetFile,
7283
}
7384
}
7485

0 commit comments

Comments
 (0)