Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,9 +120,9 @@ func Run(content embed.FS) {

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

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

firstUse := checkFirstUse()

p := tea.NewProgram(internal.InitialModel(firstFilePanelDirs, firstUse),
p := tea.NewProgram(internal.InitialModel(firstPanelPaths, firstUse),
tea.WithAltScreen(), tea.WithMouseCellMotion())
if _, err := p.Run(); err != nil {
utils.PrintfAndExitf("Alas, there's been an error: %v", err)
Expand Down
28 changes: 14 additions & 14 deletions src/internal/config_function.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import (
// themes) setted up. Processes input directories and returns toggle states.

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

updateFirstFilePanelDirs(firstFilePanelDirs, cwd, zClient)
updateFirstFilePanelPaths(firstPanelPaths, cwd, zClient)

slog.Debug("Directory configuration", "cwd", cwd, "start_directories", firstFilePanelDirs)
slog.Debug("Directory configuration", "cwd", cwd, "start_paths", firstPanelPaths)
printRuntimeInfo()

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

func updateFirstFilePanelDirs(firstFilePanelDirs []string, cwd string, zClient *zoxidelib.Client) {
for i := range firstFilePanelDirs {
if firstFilePanelDirs[i] == "" {
firstFilePanelDirs[i] = common.Config.DefaultDirectory
func updateFirstFilePanelPaths(firstPanelPaths []string, cwd string, zClient *zoxidelib.Client) {
for i := range firstPanelPaths {
if firstPanelPaths[i] == "" {
firstPanelPaths[i] = common.Config.DefaultDirectory
}
originalPath := firstFilePanelDirs[i]
firstFilePanelDirs[i] = utils.ResolveAbsPath(cwd, firstFilePanelDirs[i])
if _, err := os.Stat(firstFilePanelDirs[i]); err != nil {
slog.Error("cannot get stats for firstFilePanelDir", "error", err)
originalPath := firstPanelPaths[i]
firstPanelPaths[i] = utils.ResolveAbsPath(cwd, firstPanelPaths[i])
if _, err := os.Stat(firstPanelPaths[i]); err != nil {
slog.Error("cannot get stats", "path", firstPanelPaths[i], "error", err)
// In case the path provided did not exist, use zoxide query
// else, fallback to home dir
if common.Config.ZoxideSupport && zClient != nil {
path, err := attemptZoxideForInitPath(originalPath, zClient)
if err != nil {
slog.Error("Zoxide query error", "originalPath", originalPath, "error", err)
firstFilePanelDirs[i] = variable.HomeDir
firstPanelPaths[i] = variable.HomeDir
} else {
firstFilePanelDirs[i] = path
firstPanelPaths[i] = path
}
} else {
firstFilePanelDirs[i] = variable.HomeDir
firstPanelPaths[i] = variable.HomeDir
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/internal/default_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,15 @@ import (
// TODO: Move the configuration parameters to a ModelConfig struct.
// Something like `RendererConfig` struct for `Renderer` struct in ui/renderer package
func defaultModelConfig(toggleDotFile, toggleFooter, firstUse bool,
firstFilePanelDirs []string, zClient *zoxidelib.Client) *model {
firstPanelPaths []string, zClient *zoxidelib.Client) *model {
return &model{
filePanelFocusIndex: 0,
focusPanel: nonePanelFocus,
processBarModel: processbar.New(),
sidebarModel: sidebar.New(),
fileMetaData: metadata.New(),
fileModel: fileModel{
filePanels: filePanelSlice(firstFilePanelDirs),
filePanels: filePanelSlice(firstPanelPaths),
filePreview: preview.New(),
width: 10,
},
Expand Down
15 changes: 15 additions & 0 deletions src/internal/handle_panel_up_down.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,21 @@ package internal

// ======================================== File panel controller ========================================

func (panel *filePanel) scrollToCursor(mainPanelHeight int) {
if panel.cursor < 0 || panel.cursor >= len(panel.element) {
panel.cursor = 0
panel.render = 0
return
}

renderCount := panelElementHeight(mainPanelHeight)
if panel.cursor < panel.render {
panel.render = max(0, panel.cursor-renderCount+1)
} else if panel.cursor > panel.render+renderCount-1 {
panel.render = panel.cursor - renderCount + 1
}
}

// Control file panel list up
func (panel *filePanel) listUp(mainPanelHeight int) {
if len(panel.element) == 0 {
Expand Down
98 changes: 62 additions & 36 deletions src/internal/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,9 @@ var (
// is passed to tea.NewProgram() which accepts tea.Model
// Either way type 'model' is not exported, so there is not way main package can
// be aware of it, and use it directly
func InitialModel(firstFilePanelDirs []string, firstUseCheck bool) tea.Model {
toggleDotFile, toggleFooter, zClient := initialConfig(firstFilePanelDirs)
return defaultModelConfig(toggleDotFile, toggleFooter, firstUseCheck, firstFilePanelDirs, zClient)
func InitialModel(firstPanelPaths []string, firstUseCheck bool) tea.Model {
toggleDotFile, toggleFooter, zClient := initialConfig(firstPanelPaths)
return defaultModelConfig(toggleDotFile, toggleFooter, firstUseCheck, firstPanelPaths, zClient)
}

// Init function to be called by Bubble tea framework, sets windows title,
Expand Down Expand Up @@ -696,50 +696,76 @@ func getMaxW(s string) int {
// Render and update file panel items. Check for changes and updates in files and
// folders in the current directory.
func (m *model) getFilePanelItems() {
focusPanel := m.fileModel.filePanels[m.filePanelFocusIndex]
for i, filePanel := range m.fileModel.filePanels {
var fileElement []element
focusPanel := &m.fileModel.filePanels[m.filePanelFocusIndex]
for i := range m.fileModel.filePanels {
filePanel := &m.fileModel.filePanels[i]
nowTime := time.Now()
// Check last time each element was updated, if less then 3 seconds ignore
if !filePanel.isFocused && nowTime.Sub(filePanel.lastTimeGetElement) < 3*time.Second {
// TODO : revisit this. This feels like a duct tape solution of an actual
// deep rooted problem. This feels very hacky.
if !m.updatedToggleDotFile {
continue
if !m.shouldSkipPanelUpdate(filePanel, focusPanel, nowTime) {
// Load elements for this panel (with/without search filter)
filePanel.element = m.getElementsForPanel(filePanel)
// Update file panel list
filePanel.lastTimeGetElement = nowTime

// For hover to file on first time loading
if filePanel.targetFile != "" {
filePanel.applyTargetFileCursor()
}
}
// Due to applyTargetFileCursor, cursor might go out of range
filePanel.scrollToCursor(m.mainPanelHeight)
}

focusPanelReRender := false
m.updatedToggleDotFile = false
}

if len(focusPanel.element) > 0 {
if filepath.Dir(focusPanel.element[0].location) != focusPanel.location {
focusPanelReRender = true
}
} else {
focusPanelReRender = true
// Helper to decide whether to skip updating a panel this tick.
func (m *model) shouldSkipPanelUpdate(filePanel *filePanel, focusPanel *filePanel, nowTime time.Time) bool {
// Throttle non-focused panels unless dotfile toggle changed
if !filePanel.isFocused && nowTime.Sub(filePanel.lastTimeGetElement) < 3*time.Second {
if !m.updatedToggleDotFile {
return true
}
}

reRenderTime := int(float64(len(filePanel.element)) / 100)
focusPanelReRender := focusPanel.needsReRender()
reRenderTime := int(float64(len(filePanel.element)) / 100)
if filePanel.isFocused && !focusPanelReRender &&
nowTime.Sub(filePanel.lastTimeGetElement) < time.Duration(reRenderTime)*time.Second {
return true
}
return false
}

if filePanel.isFocused && !focusPanelReRender &&
nowTime.Sub(filePanel.lastTimeGetElement) < time.Duration(reRenderTime)*time.Second {
continue
}
// Checks whether the focus panel directory changed and forces a re-render.
func (panel *filePanel) needsReRender() bool {
if len(panel.element) > 0 {
return filepath.Dir(panel.element[0].location) != panel.location
}
return true
}

// Get file names based on search bar filter
if filePanel.searchBar.Value() != "" {
fileElement = returnDirElementBySearchString(filePanel.location, m.toggleDotFile,
filePanel.searchBar.Value(), filePanel.sortOptions.data)
} else {
fileElement = returnDirElement(filePanel.location, m.toggleDotFile, filePanel.sortOptions.data)
}
// Update file panel list
filePanel.element = fileElement
m.fileModel.filePanels[i].element = fileElement
m.fileModel.filePanels[i].lastTimeGetElement = nowTime
// Retrieves elements for a panel based on search bar value and sort options.
func (m *model) getElementsForPanel(filePanel *filePanel) []element {
if filePanel.searchBar.Value() != "" {
return returnDirElementBySearchString(
filePanel.location,
m.toggleDotFile,
filePanel.searchBar.Value(),
filePanel.sortOptions.data,
)
}
return returnDirElement(filePanel.location, m.toggleDotFile, filePanel.sortOptions.data)
}

m.updatedToggleDotFile = false
// Applies targetFile cursor positioning, if configured for the panel.
func (panel *filePanel) applyTargetFileCursor() {
for idx, el := range panel.element {
if el.name == panel.targetFile {
panel.cursor = idx
break
}
}
panel.targetFile = ""
}

// Close superfile application. Cd into the current dir if CdOnQuit on and save
Expand Down
1 change: 0 additions & 1 deletion src/internal/model_navigation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,6 @@ func TestFilePanelNavigation(t *testing.T) {
m := defaultTestModel(tt.startDir)
m.getFocusedFilePanel().cursor = tt.startCursor
m.getFocusedFilePanel().render = tt.startRender
m.getFocusedFilePanel().searchBar.SetValue("asdf")
for _, s := range tt.keyInput {
TeaUpdate(m, utils.TeaRuneKeyMsg(s))
}
Expand Down
37 changes: 37 additions & 0 deletions src/internal/model_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"path/filepath"
"testing"

tea "github.com/charmbracelet/bubbletea"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

Expand Down Expand Up @@ -95,6 +97,41 @@ func TestBasic(t *testing.T) {
})
}

func TestInitialFilePathPositionsCursorWindow(t *testing.T) {
curTestDir := t.TempDir()
dir1 := filepath.Join(curTestDir, "dir1")

utils.SetupDirectories(t, curTestDir, dir1)

var file7 string
var file2 string
for i := range 10 {
f := filepath.Join(dir1, fmt.Sprintf("file%d.txt", i))
utils.SetupFiles(t, f)
if i == 7 {
file7 = f
}
if i == 2 {
file2 = f
}
}

m := defaultTestModel(dir1, file2, file7)
// View port of 5
TeaUpdate(m, tea.WindowSizeMsg{Width: common.MinimumWidth, Height: 10})
// Uncomment below to understand the distribution
// t.Logf("Heights : %d [%d - [%d] %d]\n", m.fullHeight, m.footerHeight, m.mainPanelHeight,
// panelElementHeight(m.mainPanelHeight))
require.Len(t, m.fileModel.filePanels, 3)
assert.Equal(t, dir1, m.fileModel.filePanels[0].location)
assert.Equal(t, file2, m.fileModel.filePanels[1].getSelectedItem().location)
assert.Equal(t, 2, m.fileModel.filePanels[1].cursor)
assert.Equal(t, 0, m.fileModel.filePanels[1].render)
assert.Equal(t, file7, m.fileModel.filePanels[2].getSelectedItem().location)
assert.Equal(t, 7, m.fileModel.filePanels[2].cursor)
assert.Equal(t, 3, m.fileModel.filePanels[2].render)
}

func TestQuit(t *testing.T) {
// Test
// 1 - Normal quit
Expand Down
1 change: 1 addition & 0 deletions src/internal/type.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ type filePanel struct {
renaming bool
searchBar textinput.Model
lastTimeGetElement time.Time
targetFile string // filename to position cursor on after load
}

// Sort options
Expand Down
23 changes: 17 additions & 6 deletions src/internal/type_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package internal

import (
"fmt"
"os"
"path/filepath"

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

// ================ filepanel

func filePanelSlice(dir []string) []filePanel {
res := make([]filePanel, len(dir))
for i := range dir {
func filePanelSlice(paths []string) []filePanel {
res := make([]filePanel, len(paths))
for i := range paths {
// Making the first panel as the focussed
isFocus := i == 0
res[i] = defaultFilePanel(dir[i], isFocus)
res[i] = defaultFilePanel(paths[i], isFocus)
}
return res
}

func defaultFilePanel(dir string, focused bool) filePanel {
func defaultFilePanel(path string, focused bool) filePanel {
targetFile := ""
panelPath := path
// If path refers to a file, switch to its parent and remember the filename
if stat, err := os.Stat(panelPath); err == nil && !stat.IsDir() {
targetFile = filepath.Base(panelPath)
panelPath = filepath.Dir(panelPath)
}

return filePanel{
render: 0,
cursor: 0,
location: dir,
location: panelPath,
sortOptions: sortOptionsModel{
width: 20,
height: 4,
Expand All @@ -69,6 +79,7 @@ func defaultFilePanel(dir string, focused bool) filePanel {
isFocused: focused,
directoryRecords: make(map[string]directoryRecord),
searchBar: common.GenerateSearchBar(),
targetFile: targetFile,
}
}

Expand Down
Loading