Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
26 changes: 13 additions & 13 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 {
originalPath := firstPanelPaths[i]
firstPanelPaths[i] = utils.ResolveAbsPath(cwd, firstPanelPaths[i])
if _, err := os.Stat(firstPanelPaths[i]); err != nil {
slog.Error("cannot get stats for firstFilePanelDir", "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
97 changes: 61 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,75 @@ 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
}
}
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