Skip to content
Open
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
1 change: 1 addition & 0 deletions src/internal/common/config_type.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ type HotkeysType struct {
OpenCurrentDirectoryWithEditor []string `toml:"open_current_directory_with_editor"`

PinnedDirectory []string `toml:"pinned_directory" comment:"other"`
GotoPinned []string `toml:"goto_pinned"`
ToggleDotFile []string `toml:"toggle_dot_file"`
ChangePanelMode []string `toml:"change_panel_mode"`
OpenHelpMenu []string `toml:"open_help_menu"`
Expand Down
2 changes: 2 additions & 0 deletions src/internal/default_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/yorukot/superfile/src/internal/ui/sidebar"

"github.com/yorukot/superfile/src/internal/common"
"github.com/yorukot/superfile/src/internal/ui/pinnedmodal"
"github.com/yorukot/superfile/src/internal/ui/prompt"
zoxideui "github.com/yorukot/superfile/src/internal/ui/zoxide"
)
Expand All @@ -37,6 +38,7 @@ func defaultModelConfig(toggleDotFile, toggleFooter, firstUse bool,
helpMenu: helpmenu.New(),
promptModal: prompt.DefaultModel(prompt.PromptMinHeight, prompt.PromptMinWidth),
zoxideModal: zoxideui.DefaultModel(zoxideui.ZoxideMinHeight, zoxideui.ZoxideMinWidth, zClient),
pinnedModal: pinnedmodal.DefaultModel(pinnedmodal.PinnedModalMinHeight, pinnedmodal.PinnedModalMinWidth),
sortModal: sortmodel.New(),
zClient: zClient,
modelQuitState: notQuitting,
Expand Down
2 changes: 2 additions & 0 deletions src/internal/key_function.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ func (m *model) mainKey(msg string) tea.Cmd { //nolint: gocyclo,cyclop,funlen //
m.promptModal.Open(false)
case slices.Contains(common.Hotkeys.OpenZoxide, msg):
return m.zoxideModal.Open()
case slices.Contains(common.Hotkeys.GotoPinned, msg):
return m.openPinnedModal()

case slices.Contains(common.Hotkeys.OpenHelpMenu, msg):
m.helpMenu.Open()
Expand Down
65 changes: 59 additions & 6 deletions src/internal/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,22 @@ import (
"strings"
"time"

"github.com/barasher/go-exiftool"
"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"

"github.com/yorukot/superfile/src/config/icon"
"github.com/yorukot/superfile/src/internal/common"

"github.com/yorukot/superfile/src/internal/ui/filepanel"
"github.com/yorukot/superfile/src/internal/ui/metadata"
"github.com/yorukot/superfile/src/internal/ui/notify"
"github.com/yorukot/superfile/src/internal/ui/pinnedmodal"
"github.com/yorukot/superfile/src/internal/ui/preview"
"github.com/yorukot/superfile/src/internal/ui/sidebar"
"github.com/yorukot/superfile/src/internal/utils"

"github.com/barasher/go-exiftool"
"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"

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 @@ -82,10 +84,12 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {

// 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
// Cannot do it like processBar messages
case zoxideui.UpdateMsg:
slog.Debug("Got ModelUpdate message", "id", msg.GetReqID())
updateCmd = msg.Apply(&m.zoxideModal)
case pinnedmodal.UpdateMsg:
updateCmd = msg.Apply(&m.pinnedModal)

// Its a pain to interconvert commands like processBar
case preview.UpdateMsg:
Expand Down Expand Up @@ -209,6 +213,7 @@ func (m *model) updateComponentDimensions() tea.Cmd {
m.setHelpMenuSize()
m.setPromptModelSize()
m.setZoxideModelSize()
m.setPinnedModalSize()
m.setFooterComponentSize()

// File preview panel requires explicit height update, unlike sidebar/file panels
Expand Down Expand Up @@ -255,6 +260,14 @@ func (m *model) setZoxideModelSize() {
m.zoxideModal.SetWidth(m.fullWidth / 2) //nolint:mnd // modal uses half width for layout
}

func (m *model) setPinnedModalSize() {
// Scale pinned modal's maxHeight - 50% of total height to accommodate scroll indicators
m.pinnedModal.SetMaxHeight(m.fullHeight / 2) //nolint:mnd // modal uses half height for layout

// Scale pinned modal's width - 75% of total width
m.pinnedModal.SetWidth(m.fullWidth * 3 / 4) //nolint:mnd // modal uses 3/4 width for layout
}

func (m *model) setFooterComponentSize() {
var width, clipBoardwidth, height int
height = m.footerHeight + common.BorderPadding
Expand Down Expand Up @@ -300,6 +313,9 @@ func (m *model) handleKeyInput(msg tea.KeyMsg) tea.Cmd {
case m.zoxideModal.IsOpen():
// Ignore keypress. It will be handled in Update call via
// updateFilePanelState
case m.pinnedModal.IsOpen():
// Ignore keypress. It will be handled in Update call via
// updateFilePanelState

// Handles all warn models except the warn model for confirming to quit
case m.notifyModel.IsOpen():
Expand Down Expand Up @@ -375,6 +391,9 @@ func (m *model) updateComponentState(msg tea.Msg) tea.Cmd {
case m.zoxideModal.IsOpen():
action, cmd = m.zoxideModal.HandleUpdate(msg)
cmd = tea.Batch(cmd, m.applyZoxideModalAction(action))
case m.pinnedModal.IsOpen():
action, cmd = m.pinnedModal.HandleUpdate(msg)
cmd = tea.Batch(cmd, m.applyPinnedModalAction(action))
}
return cmd
}
Expand Down Expand Up @@ -423,6 +442,12 @@ func (m *model) applyZoxideModalAction(action common.ModelAction) tea.Cmd {
return cmd
}

// Apply the Action for pinned modal (no result notifications needed)
func (m *model) applyPinnedModalAction(action common.ModelAction) tea.Cmd {
_, cmd, _ := m.logAndExecuteAction(action)
return cmd
}

// TODO : Move them around to appropriate places
func (m *model) applyShellCommandAction(shellCommand string) {
focusPanelDir := m.getFocusedFilePanel().Location
Expand All @@ -442,6 +467,27 @@ func (m *model) splitPanel() (tea.Cmd, error) {
return m.fileModel.CreateNewFilePanel(m.getFocusedFilePanel().Location)
}

func (m *model) openPinnedModal() tea.Cmd {
var convertedDirs []pinnedmodal.Directory

pinnedDirs := m.sidebarModel.GetPinnedDirectories()
if len(pinnedDirs) == 0 && m.sidebarModel.Disabled() {
pinnedMgr := sidebar.NewPinnedFileManager(variable.PinnedFile)
pinnedDirs = pinnedMgr.Load()
slog.Debug("Loaded pinned directories from file (sidebar disabled)", "count", len(pinnedDirs))
}

convertedDirs = make([]pinnedmodal.Directory, 0, len(pinnedDirs))
for _, dir := range pinnedDirs {
convertedDirs = append(convertedDirs, pinnedmodal.Directory{
Location: dir.Location,
Name: dir.Name,
})
}
m.pinnedModal.LoadPinnedDirs(convertedDirs)
return m.pinnedModal.Open()
}

func (m *model) createNewFilePanelRelativeToCurrent(path string) (tea.Cmd, error) {
currentDir := m.getFocusedFilePanel().Location
return m.fileModel.CreateNewFilePanel(utils.ResolveAbsPath(currentDir, path))
Expand Down Expand Up @@ -533,6 +579,13 @@ func (m *model) updateRenderForOverlay(finalRender string) string {
return stringfunction.PlaceOverlay(overlayX, overlayY, zoxideModal, finalRender)
}

if m.pinnedModal.IsOpen() {
pinnedModal := m.pinnedModalRender()
overlayX := m.fullWidth/common.CenterDivisor - m.pinnedModal.GetWidth()/common.CenterDivisor
overlayY := m.fullHeight/common.CenterDivisor - m.pinnedModal.GetMaxHeight()/common.CenterDivisor
return stringfunction.PlaceOverlay(overlayX, overlayY, pinnedModal, finalRender)
}

if m.sortModal.IsOpen() {
sortOptions := m.sortModal.Render()
overlayX := m.fullWidth/common.CenterDivisor - m.sortModal.Width/common.CenterDivisor
Expand Down
4 changes: 4 additions & 0 deletions src/internal/model_render.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,3 +128,7 @@ func (m *model) promptModalRender() string {
func (m *model) zoxideModalRender() string {
return m.zoxideModal.Render()
}

func (m *model) pinnedModalRender() string {
return m.pinnedModal.Render()
}
2 changes: 2 additions & 0 deletions src/internal/type.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
zoxidelib "github.com/lazysegtree/go-zoxide"

"github.com/yorukot/superfile/src/internal/ui/helpmenu"
pinnedmodalui "github.com/yorukot/superfile/src/internal/ui/pinnedmodal"

"github.com/yorukot/superfile/src/internal/ui/clipboard"
"github.com/yorukot/superfile/src/internal/ui/sortmodel"
Expand Down Expand Up @@ -61,6 +62,7 @@ type model struct {
helpMenu helpmenu.Model
promptModal prompt.Model
zoxideModal zoxideui.Model
pinnedModal pinnedmodalui.Model
sortModal sortmodel.Model

// Zoxide client for directory tracking
Expand Down
5 changes: 5 additions & 0 deletions src/internal/ui/helpmenu/data.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ func getData() []hotkeydata { //nolint: funlen // This should be self contained
description: "Open zoxide navigation",
hotkeyWorkType: globalType,
},
{
hotkey: common.Hotkeys.GotoPinned,
description: "Goto pinned directory",
hotkeyWorkType: globalType,
},
{
subTitle: "Panel navigation",
},
Expand Down
12 changes: 12 additions & 0 deletions src/internal/ui/pinnedmodal/consts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package pinnedmodal

const (
pinnedModalHeadlineText = "Goto Pinned Directory"

PinnedModalMinWidth = 20
PinnedModalMinHeight = 3

maxVisibleResults = 5

columnWidth = 15
)
141 changes: 141 additions & 0 deletions src/internal/ui/pinnedmodal/model.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package pinnedmodal

import (
"slices"

tea "github.com/charmbracelet/bubbletea"

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

func DefaultModel(maxHeight int, width int) Model {
return GenerateModel(maxHeight, width)
}

func GenerateModel(maxHeight int, width int) Model {
m := Model{
headline: pinnedModalHeadlineText,
open: false,
textInput: common.GeneratePromptTextInput(),
results: []Directory{},
}
m.SetMaxHeight(maxHeight)
m.SetWidth(width)
m.textInput.Prompt = ""
return m
}

func (m *Model) HandleUpdate(msg tea.Msg) (common.ModelAction, tea.Cmd) {
var action common.ModelAction
action = common.NoAction{}
var cmd tea.Cmd
if !m.IsOpen() {
return action, cmd
}

switch msg := msg.(type) {
case tea.KeyMsg:
switch {
case slices.Contains(common.Hotkeys.ConfirmTyping, msg.String()):
action = m.handleConfirm()
m.Close()
case slices.Contains(common.Hotkeys.CancelTyping, msg.String()),
slices.Contains(common.Hotkeys.Quit, msg.String()):
m.Close()
case slices.Contains(common.Hotkeys.ListUp, msg.String()):
m.navigateUp()
case slices.Contains(common.Hotkeys.ListDown, msg.String()):
m.navigateDown()
case slices.Contains(common.Hotkeys.PageUp, msg.String()):
m.navigatePageUp()
case slices.Contains(common.Hotkeys.PageDown, msg.String()):
m.navigatePageDown()
case slices.Contains(common.Hotkeys.GotoPinned, msg.String()) && m.justOpened:
m.justOpened = false
default:
cmd = m.handleNormalKeyInput(msg)
}
default:
m.textInput, cmd = m.textInput.Update(msg)
}
return action, cmd
}
Comment thread
robert-zaremba marked this conversation as resolved.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

func (m *Model) handleConfirm() common.ModelAction {
if len(m.results) > 0 && m.cursor >= 0 && m.cursor < len(m.results) {
selectedDir := m.results[m.cursor]
return common.CDCurrentPanelAction{
Location: selectedDir.Location,
}
}
return common.NoAction{}
}

func (m *Model) handleNormalKeyInput(msg tea.KeyMsg) tea.Cmd {
var cmd tea.Cmd
m.textInput, cmd = m.textInput.Update(msg)
return tea.Batch(cmd, m.GetQueryCmd(m.textInput.Value()))
}

func (m *Model) GetQueryCmd(query string) tea.Cmd {
reqID := m.reqCnt
allDirs := m.allDirs
m.reqCnt++
return func() tea.Msg {
results := filterPinnedDirs(query, allDirs)
return NewUpdateMsg(query, results, reqID, "")
}
}

func filterPinnedDirs(query string, allDirs []Directory) []Directory {
if query == "" {
return allDirs
}

if len(allDirs) == 0 {
return []Directory{}
}

var filteredDirs []Directory

haystack := make([]string, len(allDirs))
for i, dir := range allDirs {
searchText := dir.Name + " " + dir.Location
haystack[i] = searchText
}

for _, match := range utils.FzfSearch(query, haystack) {
if match.HayIndex >= 0 && int(match.HayIndex) < len(allDirs) {
filteredDirs = append(filteredDirs, allDirs[match.HayIndex])
}
}

return filteredDirs
}

func (m *Model) LoadPinnedDirs(dirs []Directory) {
m.allDirs = dirs
m.results = dirs
m.cursor = 0
m.renderIndex = 0
}

func (m *Model) FilterPinnedDirs(query string) {
m.results = filterPinnedDirs(query, m.allDirs)
m.cursor = 0
m.renderIndex = 0
}

func (msg UpdateMsg) Apply(m *Model) tea.Cmd {
currentQuery := m.textInput.Value()
if msg.query != currentQuery {
return nil
}

m.results = msg.results
m.cursor = 0
m.renderIndex = 0

return nil
}
Loading