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
2 changes: 1 addition & 1 deletion src/internal/common/predefined_variable.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const TrashWarnContent = "This operation will move file or directory to trash ca
const PermanentDeleteWarnTitle = "Are you sure you want to completely delete"
const PermanentDeleteWarnContent = "This operation cannot be undone and your data will be completely lost."

var (
const (
MinimumHeight = 24
MinimumWidth = 60

Expand Down
2 changes: 1 addition & 1 deletion src/internal/key_function.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ func (m *model) mainKey(msg string) tea.Cmd { //nolint: gocyclo,cyclop,funlen //
case slices.Contains(common.Hotkeys.OpenSPFPrompt, msg):
m.promptModal.Open(false)
case slices.Contains(common.Hotkeys.OpenZoxide, msg):
m.zoxideModal.Open()
return m.zoxideModal.Open()

case slices.Contains(common.Hotkeys.OpenHelpMenu, msg):
m.openHelpMenu()
Expand Down
14 changes: 10 additions & 4 deletions src/internal/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"github.com/charmbracelet/x/ansi"

variable "github.com/yorukot/superfile/src/config"
zoxideui "github.com/yorukot/superfile/src/internal/ui/zoxide"
stringfunction "github.com/yorukot/superfile/src/pkg/string_function"
)

Expand Down Expand Up @@ -85,6 +86,14 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.handleMouseMsg(msg)
case tea.KeyMsg:
inputCmd = m.handleKeyInput(msg)

// Has to handle zoxide messages separately as they could be generated via
// zoxide update commands, or batched commands from textinput
// Cannot do it like processbar messages
case zoxideui.UpdateMsg:
slog.Debug("Got ModelUpdate message", "id", msg.GetReqID())
gotModelUpdateMsg = true
updateCmd = msg.Apply(&m.zoxideModal)
case ModelUpdateMessage:
// TODO: Some of these updates messages should trigger filePanel state update
// For example a success message for delete operation
Expand Down Expand Up @@ -411,6 +420,7 @@ func (m *model) handleKeyInput(msg tea.KeyMsg) tea.Cmd {
func (m *model) updateFilePanelsState(msg tea.Msg) tea.Cmd {
focusPanel := &m.fileModel.filePanels[m.filePanelFocusIndex]
var cmd tea.Cmd
var action common.ModelAction
switch {
case m.firstTextInput:
m.firstTextInput = false
Expand All @@ -421,15 +431,11 @@ func (m *model) updateFilePanelsState(msg tea.Msg) tea.Cmd {
case m.typingModal.open:
m.typingModal.textInput, cmd = m.typingModal.textInput.Update(msg)
case m.promptModal.IsOpen():
// *cmd is a non-name, and cannot be used on left of :=
var action common.ModelAction
// Taking returned cmd is necessary for blinking
// TODO : Separate this to a utility
cwdLocation := m.fileModel.filePanels[m.filePanelFocusIndex].location
action, cmd = m.promptModal.HandleUpdate(msg, cwdLocation)
m.applyPromptModalAction(action)
case m.zoxideModal.IsOpen():
var action common.ModelAction
action, cmd = m.zoxideModal.HandleUpdate(msg)
m.applyZoxideModalAction(action)
}
Expand Down
71 changes: 0 additions & 71 deletions src/internal/model_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,8 @@ import (
"fmt"
"os"
"path/filepath"
"runtime"
"testing"

zoxidelib "github.com/lazysegtree/go-zoxide"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

Expand Down Expand Up @@ -247,72 +245,3 @@ func TestChooserFile(t *testing.T) {
})
}
}

func TestZoxide(t *testing.T) {
zoxideDataDir := t.TempDir()
zClient, err := zoxidelib.New(zoxidelib.WithDataDir(zoxideDataDir))
if err != nil {
if runtime.GOOS != utils.OsLinux {
t.Skipf("Skipping zoxide tests in non-Linux because zoxide client cannot be initialized")
} else {
t.Fatalf("zoxide initialization failed")
}
}

originalZoxideSupport := common.Config.ZoxideSupport
defer func() {
common.Config.ZoxideSupport = originalZoxideSupport
}()

curTestDir := filepath.Join(testDir, "TestZoxide")
dir1 := filepath.Join(curTestDir, "dir1")
dir2 := filepath.Join(curTestDir, "dir2")
dir3 := filepath.Join(curTestDir, "dir3")
utils.SetupDirectories(t, curTestDir, dir1, dir2, dir3)

t.Run("Zoxide tracking and navigation", func(t *testing.T) {
common.Config.ZoxideSupport = true
m := defaultTestModelWithZClient(zClient, dir1)

err := m.updateCurrentFilePanelDir(dir2)
require.NoError(t, err, "Failed to navigate to dir2")
assert.Equal(t, dir2, m.getFocusedFilePanel().location, "Should be in dir2 after navigation")

err = m.updateCurrentFilePanelDir(dir3)
require.NoError(t, err, "Failed to navigate to dir3")
assert.Equal(t, dir3, m.getFocusedFilePanel().location, "Should be in dir3 after navigation")

TeaUpdate(m, utils.TeaRuneKeyMsg(common.Hotkeys.OpenZoxide[0]))
assert.True(t, m.zoxideModal.IsOpen(), "Zoxide modal should open when pressing 'z' key")

// Type "dir2" to search for it
for _, char := range "dir2" {
TeaUpdate(m, utils.TeaRuneKeyMsg(string(char)))
}

results := m.zoxideModal.GetResults()
assert.GreaterOrEqual(t, len(results), 1, "Should have at least 1 directory found by zoxide UI search")

resultPaths := make([]string, len(results))
for i, result := range results {
resultPaths[i] = result.Path
}
assert.Contains(t, resultPaths, dir2, "dir2 should be found by zoxide UI search")

// Press enter to navigate to dir2
TeaUpdate(m, utils.TeaRuneKeyMsg(common.Hotkeys.ConfirmTyping[0]))
assert.False(t, m.zoxideModal.IsOpen(), "Zoxide modal should close after navigation")
assert.Equal(t, dir2, m.getFocusedFilePanel().location, "Should navigate back to dir2 after zoxide selection")
})

t.Run("Zoxide disabled shows no results", func(t *testing.T) {
common.Config.ZoxideSupport = false
m := defaultTestModelWithZClient(zClient, dir1)

TeaUpdate(m, utils.TeaRuneKeyMsg(common.Hotkeys.OpenZoxide[0]))
assert.True(t, m.zoxideModal.IsOpen(), "Zoxide modal should open even when ZoxideSupport is disabled")

results := m.zoxideModal.GetResults()
assert.Empty(t, results, "Zoxide modal should show no results when ZoxideSupport is disabled")
})
}
150 changes: 150 additions & 0 deletions src/internal/model_zoxide_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
package internal

import (
"path/filepath"
"runtime"
"testing"

tea "github.com/charmbracelet/bubbletea"
zoxidelib "github.com/lazysegtree/go-zoxide"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/yorukot/superfile/src/internal/common"
"github.com/yorukot/superfile/src/internal/utils"
)

func setupProgAndOpenZoxide(t *testing.T, zClient *zoxidelib.Client, dir string) *TeaProg {
t.Helper()
common.Config.ZoxideSupport = true
m := defaultTestModelWithZClient(zClient, dir)
p := NewTestTeaProgWithEventLoop(t, m)

p.SendKey(common.Hotkeys.OpenZoxide[0])
assert.Eventually(t, func() bool {
return p.getModel().zoxideModal.IsOpen()
}, DefaultTestTimeout, DefaultTestTick, "Zoxide modal should open")
return p
}

func updateCurrentFilePanelDirOfTestModel(t *testing.T, p *TeaProg, dir string) {
err := p.getModel().updateCurrentFilePanelDir(dir)
require.NoError(t, err, "Failed to navigate to %s", dir)
assert.Equal(t, dir, p.getModel().getFocusedFilePanel().location, "Should be in %s after navigation", dir)
}

func TestZoxide(t *testing.T) {
zoxideDataDir := t.TempDir()
zClient, err := zoxidelib.New(zoxidelib.WithDataDir(zoxideDataDir))
if err != nil {
if runtime.GOOS != utils.OsLinux {
t.Skipf("Skipping zoxide tests in non-Linux because zoxide client cannot be initialized")
} else {
t.Fatalf("zoxide initialization failed")
}
}

originalZoxideSupport := common.Config.ZoxideSupport
defer func() {
common.Config.ZoxideSupport = originalZoxideSupport
}()

curTestDir := filepath.Join(testDir, "TestZoxide")
dir1 := filepath.Join(curTestDir, "dir1")
dir2 := filepath.Join(curTestDir, "dir2")
dir3 := filepath.Join(curTestDir, "dir3")
multiSpaceDir := filepath.Join(curTestDir, "test dir")
utils.SetupDirectories(t, curTestDir, dir1, dir2, dir3, multiSpaceDir)

t.Run("Zoxide tracking and navigation", func(t *testing.T) {
p := setupProgAndOpenZoxide(t, zClient, dir1)
updateCurrentFilePanelDirOfTestModel(t, p, dir2)
updateCurrentFilePanelDirOfTestModel(t, p, dir3)

p.SendKey("dir2")
assert.Eventually(t, func() bool {
results := p.getModel().zoxideModal.GetResults()
return len(results) == 1 && results[0].Path == dir2
}, DefaultTestTimeout, DefaultTestTick, "dir2 should be found by zoxide UI search")

// Press enter to navigate to dir2
p.SendKey(common.Hotkeys.ConfirmTyping[0])
assert.Eventually(t, func() bool {
return !p.getModel().zoxideModal.IsOpen()
}, DefaultTestTimeout, DefaultTestTick, "Zoxide modal should close after navigation")
assert.Equal(t, dir2, p.getModel().getFocusedFilePanel().location,
"Should navigate back to dir2 after zoxide selection")
})

t.Run("Zoxide disabled shows no results", func(t *testing.T) {
common.Config.ZoxideSupport = false
m := defaultTestModelWithZClient(zClient, dir1)

TeaUpdate(m, utils.TeaRuneKeyMsg(common.Hotkeys.OpenZoxide[0]))
assert.True(t, m.zoxideModal.IsOpen(), "Zoxide modal should open even when ZoxideSupport is disabled")

results := m.zoxideModal.GetResults()
assert.Empty(t, results, "Zoxide modal should show no results when ZoxideSupport is disabled")
})

t.Run("Zoxide modal size on window resize", func(t *testing.T) {
p := setupProgAndOpenZoxide(t, zClient, dir1)

initialWidth := p.getModel().zoxideModal.GetWidth()
initialMaxHeight := p.getModel().zoxideModal.GetMaxHeight()

p.SendDirectly(tea.WindowSizeMsg{Width: 2 * DefaultTestModelWidth, Height: 2 * DefaultTestModelHeight})

updatedWidth := p.getModel().zoxideModal.GetWidth()
updatedMaxHeight := p.getModel().zoxideModal.GetMaxHeight()
assert.Greater(t, updatedWidth, initialWidth, "Width should increase with larger window")
assert.Greater(t, updatedMaxHeight, initialMaxHeight, "MaxHeight should increase with larger window")
})

t.Run("Zoxide 'z' key suppression on open", func(t *testing.T) {
p := setupProgAndOpenZoxide(t, zClient, dir1)
assert.Empty(t, p.getModel().zoxideModal.GetTextInputValue(),
"The 'z' key should not be added to textInput")
p.SendKeyDirectly("abc")
assert.Equal(t, "abc", p.getModel().zoxideModal.GetTextInputValue())
})

t.Run("Multi-space directory name navigation", func(t *testing.T) {
p := setupProgAndOpenZoxide(t, zClient, dir1)

updateCurrentFilePanelDirOfTestModel(t, p, multiSpaceDir)
updateCurrentFilePanelDirOfTestModel(t, p, dir1)

p.SendKey(filepath.Base(multiSpaceDir))
assert.Eventually(t, func() bool {
results := p.getModel().zoxideModal.GetResults()
for _, result := range results {
if result.Path == multiSpaceDir {
return true
}
}
return false
}, DefaultTestTimeout, DefaultTestTick, "Multi-space directory should be found by zoxide")

// Reset textinput via Close-Open
p.getModel().zoxideModal.Close()
p.getModel().zoxideModal.Open()
p.SendKey("di r 1")
assert.Eventually(t, func() bool {
results := p.getModel().zoxideModal.GetResults()
for _, result := range results {
if result.Path == dir1 {
return true
}
}
return false
}, DefaultTestTimeout, DefaultTestTick, "dir1 should be found by zoxide")
})

t.Run("Zoxide escape key closes modal", func(t *testing.T) {
p := setupProgAndOpenZoxide(t, zClient, dir1)
p.SendKeyDirectly(common.Hotkeys.CancelTyping[0])
assert.False(t, p.getModel().zoxideModal.IsOpen(),
"Zoxide modal should close on escape key")
})
}
12 changes: 8 additions & 4 deletions src/internal/test_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,24 @@ import (

const DefaultTestTick = 10 * time.Millisecond
const DefaultTestTimeout = time.Second
const DefaultTestModelWidth = 2 * common.MinimumWidth
const DefaultTestModelHeight = 2 * common.MinimumHeight

// -------------------- Model setup utils

func defaultTestModel(dirs ...string) *model {
m := defaultModelConfig(false, false, false, dirs, nil)
m.disableMetadata = true
TeaUpdate(m, tea.WindowSizeMsg{Width: 2 * common.MinimumWidth, Height: 2 * common.MinimumHeight})
return m
return setModelParamsForTest(m)
}

func defaultTestModelWithZClient(zClient *zoxidelib.Client, dirs ...string) *model {
m := defaultModelConfig(false, false, false, dirs, zClient)
return setModelParamsForTest(m)
}

func setModelParamsForTest(m *model) *model {
m.disableMetadata = true
TeaUpdate(m, tea.WindowSizeMsg{Width: 2 * common.MinimumWidth, Height: 2 * common.MinimumHeight})
TeaUpdate(m, tea.WindowSizeMsg{Width: DefaultTestModelWidth, Height: DefaultTestModelHeight})
return m
}

Expand Down
Loading
Loading