feat: directory history (Alt+Left/Alt+Right), filter fixes (sticky per-pane, navigation, Shift+select, offset, Esc clear)
This commit is contained in:
parent
08c095e74f
commit
7a55fb289e
3 changed files with 242 additions and 35 deletions
|
|
@ -36,6 +36,8 @@ type KeyMap struct {
|
||||||
ProgressCancel key.Binding
|
ProgressCancel key.Binding
|
||||||
Cancel key.Binding
|
Cancel key.Binding
|
||||||
Quit key.Binding
|
Quit key.Binding
|
||||||
|
HistoryBack key.Binding
|
||||||
|
HistoryForward key.Binding
|
||||||
}
|
}
|
||||||
|
|
||||||
func DefaultKeyMap() KeyMap {
|
func DefaultKeyMap() KeyMap {
|
||||||
|
|
@ -71,6 +73,8 @@ func DefaultKeyMap() KeyMap {
|
||||||
Confirm: key.NewBinding(key.WithKeys("enter", "y"), key.WithHelp("Enter/y", "confirm")),
|
Confirm: key.NewBinding(key.WithKeys("enter", "y"), key.WithHelp("Enter/y", "confirm")),
|
||||||
Background: key.NewBinding(key.WithKeys("b"), key.WithHelp("b", "background")),
|
Background: key.NewBinding(key.WithKeys("b"), key.WithHelp("b", "background")),
|
||||||
ProgressCancel: key.NewBinding(key.WithKeys("c"), key.WithHelp("c", "cancel transfer")),
|
ProgressCancel: key.NewBinding(key.WithKeys("c"), key.WithHelp("c", "cancel transfer")),
|
||||||
|
HistoryBack: key.NewBinding(key.WithKeys("alt+left"), key.WithHelp("A-←", "back")),
|
||||||
|
HistoryForward: key.NewBinding(key.WithKeys("alt+right"), key.WithHelp("A-→", "forward")),
|
||||||
Cancel: key.NewBinding(key.WithKeys("esc"), key.WithHelp("Esc", "cancel")),
|
Cancel: key.NewBinding(key.WithKeys("esc"), key.WithHelp("Esc", "cancel")),
|
||||||
Quit: key.NewBinding(key.WithKeys("f10", "q", "ctrl+c"), key.WithHelp("F10/q", "quit")),
|
Quit: key.NewBinding(key.WithKeys("f10", "q", "ctrl+c"), key.WithHelp("F10/q", "quit")),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -208,6 +208,7 @@ type Model struct {
|
||||||
filterMode bool
|
filterMode bool
|
||||||
filterQuery string
|
filterQuery string
|
||||||
filterInput textinput.Model
|
filterInput textinput.Model
|
||||||
|
filterPaneID PaneID
|
||||||
|
|
||||||
modal modalState
|
modal modalState
|
||||||
status string
|
status string
|
||||||
|
|
@ -754,29 +755,30 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
m.status = fmt.Sprintf("Filter: %s", m.filterQuery)
|
m.status = fmt.Sprintf("Filter: %s", m.filterQuery)
|
||||||
return m, nil
|
return m, nil
|
||||||
case key.Matches(msg, m.keys.Up):
|
case key.Matches(msg, m.keys.Up):
|
||||||
m.moveCursor(-1)
|
m.moveFilteredCursor(-1)
|
||||||
return m, m.loadPreviewCmd()
|
return m, m.loadPreviewCmd()
|
||||||
case key.Matches(msg, m.keys.Down):
|
case key.Matches(msg, m.keys.Down):
|
||||||
m.moveCursor(1)
|
m.moveFilteredCursor(1)
|
||||||
return m, m.loadPreviewCmd()
|
return m, m.loadPreviewCmd()
|
||||||
case key.Matches(msg, m.keys.PageUp):
|
case key.Matches(msg, m.keys.PageUp):
|
||||||
m.moveCursor(-max(m.bodyHeight()-6, 5))
|
m.moveFilteredCursor(-max(m.bodyHeight()-6, 5))
|
||||||
return m, m.loadPreviewCmd()
|
return m, m.loadPreviewCmd()
|
||||||
case key.Matches(msg, m.keys.PageDown):
|
case key.Matches(msg, m.keys.PageDown):
|
||||||
m.moveCursor(max(m.bodyHeight()-6, 5))
|
m.moveFilteredCursor(max(m.bodyHeight()-6, 5))
|
||||||
return m, m.loadPreviewCmd()
|
return m, m.loadPreviewCmd()
|
||||||
default:
|
default:
|
||||||
var cmd tea.Cmd
|
var cmd tea.Cmd
|
||||||
m.filterInput, cmd = m.filterInput.Update(msg)
|
m.filterInput, cmd = m.filterInput.Update(msg)
|
||||||
m.filterQuery = m.filterInput.Value()
|
m.filterQuery = m.filterInput.Value()
|
||||||
m.adjustCursorForFilter()
|
m.snapFilterCursor()
|
||||||
return m, cmd
|
return m, cmd
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Toggle filter mode
|
// Toggle filter mode — attaches filter to the currently active pane
|
||||||
if key.Matches(msg, m.keys.Filter) {
|
if key.Matches(msg, m.keys.Filter) {
|
||||||
m.filterMode = true
|
m.filterMode = true
|
||||||
|
m.filterPaneID = m.active
|
||||||
m.filterInput.Focus()
|
m.filterInput.Focus()
|
||||||
m.filterInput.SetValue(m.filterQuery)
|
m.filterInput.SetValue(m.filterQuery)
|
||||||
m.status = "Filter: type to filter, Enter to confirm, Esc to clear"
|
m.status = "Filter: type to filter, Enter to confirm, Esc to clear"
|
||||||
|
|
@ -795,6 +797,12 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
m.openRenameModal()
|
m.openRenameModal()
|
||||||
return m, nil
|
return m, nil
|
||||||
case key.Matches(msg, m.keys.Cancel), msg.String() == "q":
|
case key.Matches(msg, m.keys.Cancel), msg.String() == "q":
|
||||||
|
// Esc on the pane where filter is active clears it.
|
||||||
|
if m.filterQuery != "" && m.filterPaneID == m.active {
|
||||||
|
m.clearFilter()
|
||||||
|
m.status = "Filter cleared"
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
if m.infoMode {
|
if m.infoMode {
|
||||||
m.infoMode = false
|
m.infoMode = false
|
||||||
m.selectMode = false
|
m.selectMode = false
|
||||||
|
|
@ -868,6 +876,10 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
m.status = err.Error()
|
m.status = err.Error()
|
||||||
}
|
}
|
||||||
return m, m.loadPreviewCmd()
|
return m, m.loadPreviewCmd()
|
||||||
|
case key.Matches(msg, m.keys.HistoryBack):
|
||||||
|
return m.historyBack()
|
||||||
|
case key.Matches(msg, m.keys.HistoryForward):
|
||||||
|
return m.historyForward()
|
||||||
case key.Matches(msg, m.keys.Refresh):
|
case key.Matches(msg, m.keys.Refresh):
|
||||||
return m.refreshAllPanes("Refreshed")
|
return m.refreshAllPanes("Refreshed")
|
||||||
case key.Matches(msg, m.keys.DirSize):
|
case key.Matches(msg, m.keys.DirSize):
|
||||||
|
|
@ -906,9 +918,18 @@ func (m Model) View() string {
|
||||||
Background(m.palette.Panel).
|
Background(m.palette.Panel).
|
||||||
Render("")
|
Render("")
|
||||||
|
|
||||||
// Use filtered panes when a filter query is active.
|
// Filter is sticky: once activated on a pane it stays on that pane
|
||||||
leftPane := m.filteredPane(m.left)
|
// even after switching to the other pane with Tab.
|
||||||
rightPane := m.filteredPane(m.right)
|
leftPane := m.left
|
||||||
|
rightPane := m.right
|
||||||
|
if m.filterQuery != "" {
|
||||||
|
switch m.filterPaneID {
|
||||||
|
case PaneLeft:
|
||||||
|
leftPane = m.filteredPane(m.left)
|
||||||
|
case PaneRight:
|
||||||
|
rightPane = m.filteredPane(m.right)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var panels string
|
var panels string
|
||||||
if m.viewMode && m.previewData.Kind == vfs.PreviewKindImage {
|
if m.viewMode && m.previewData.Kind == vfs.PreviewKindImage {
|
||||||
|
|
@ -1177,25 +1198,85 @@ func (m *Model) refreshAllPanes(status string) (tea.Model, tea.Cmd) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) moveCursor(delta int) {
|
func (m *Model) moveCursor(delta int) {
|
||||||
|
// When a filter query is active on this pane, move through filtered entries
|
||||||
|
// only, so the cursor always lands on a matching item.
|
||||||
|
if m.filterQuery != "" && m.filterPaneID == m.active {
|
||||||
|
m.moveFilteredCursor(delta)
|
||||||
|
return
|
||||||
|
}
|
||||||
pane := m.activePane()
|
pane := m.activePane()
|
||||||
pane.Move(delta, max(m.bodyHeight()-4, 1))
|
pane.Move(delta, max(m.bodyHeight()-4, 1))
|
||||||
m.hover = hoverState{}
|
m.hover = hoverState{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// adjustCursorForFilter ensures the cursor stays within bounds of the
|
// snapFilterCursor moves the real cursor to the nearest entry matching the
|
||||||
// filtered entry list. Called after each filter keystroke.
|
// current filter query. Called after each filter keystroke so the user
|
||||||
func (m *Model) adjustCursorForFilter() {
|
// always sees a selected item in the filtered view.
|
||||||
|
func (m *Model) snapFilterCursor() {
|
||||||
pane := m.activePane()
|
pane := m.activePane()
|
||||||
if m.filterQuery == "" {
|
if m.filterQuery == "" || len(pane.Entries) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
count := m.filteredCount(pane)
|
query := strings.ToLower(m.filterQuery)
|
||||||
if pane.Cursor >= count {
|
|
||||||
pane.Cursor = max(count-1, 0)
|
// If current cursor position already matches, keep it.
|
||||||
|
if pane.Cursor >= 0 && pane.Cursor < len(pane.Entries) {
|
||||||
|
entry := pane.Entries[pane.Cursor]
|
||||||
|
if entry.IsParent || strings.Contains(strings.ToLower(entry.DisplayName()), query) {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
if pane.Offset > pane.Cursor {
|
}
|
||||||
|
|
||||||
|
// Search forward from cursor.
|
||||||
|
for i := pane.Cursor + 1; i < len(pane.Entries); i++ {
|
||||||
|
entry := pane.Entries[i]
|
||||||
|
if entry.IsParent || strings.Contains(strings.ToLower(entry.DisplayName()), query) {
|
||||||
|
pane.Cursor = i
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search backward from cursor.
|
||||||
|
for i := pane.Cursor - 1; i >= 0; i-- {
|
||||||
|
entry := pane.Entries[i]
|
||||||
|
if entry.IsParent || strings.Contains(strings.ToLower(entry.DisplayName()), query) {
|
||||||
|
pane.Cursor = i
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// moveFilteredCursor moves the real cursor to the next/prev entry matching
|
||||||
|
// the current filter query. Used for Up/Down navigation in filter mode.
|
||||||
|
func (m *Model) moveFilteredCursor(delta int) {
|
||||||
|
pane := m.activePane()
|
||||||
|
if m.filterQuery == "" || len(pane.Entries) == 0 {
|
||||||
|
m.moveCursor(delta)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
query := strings.ToLower(m.filterQuery)
|
||||||
|
maxIdx := len(pane.Entries) - 1
|
||||||
|
|
||||||
|
idx := pane.Cursor + delta
|
||||||
|
for idx >= 0 && idx <= maxIdx {
|
||||||
|
entry := pane.Entries[idx]
|
||||||
|
if entry.IsParent || strings.Contains(strings.ToLower(entry.DisplayName()), query) {
|
||||||
|
pane.Cursor = idx
|
||||||
|
pageSize := max(m.bodyHeight()-4, 1)
|
||||||
|
if pane.Cursor < pane.Offset {
|
||||||
pane.Offset = pane.Cursor
|
pane.Offset = pane.Cursor
|
||||||
}
|
}
|
||||||
|
if pageSize > 0 && pane.Cursor >= pane.Offset+pageSize {
|
||||||
|
pane.Offset = pane.Cursor - pageSize + 1
|
||||||
|
}
|
||||||
|
if pane.Offset < 0 {
|
||||||
|
pane.Offset = 0
|
||||||
|
}
|
||||||
|
m.hover = hoverState{}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
idx += delta
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// filteredCount returns the number of entries matching the current filter.
|
// filteredCount returns the number of entries matching the current filter.
|
||||||
|
|
@ -1214,24 +1295,40 @@ func (m *Model) filteredCount(pane *BrowserPane) int {
|
||||||
}
|
}
|
||||||
|
|
||||||
// filteredPane returns a copy of the pane with entries filtered by the current query.
|
// filteredPane returns a copy of the pane with entries filtered by the current query.
|
||||||
|
// The cursor in the returned copy reflects the position of the real cursor
|
||||||
|
// within the filtered subset, so Selected() on the original pane still returns
|
||||||
|
// the correct entry. Offset is recomputed in filtered-entry space so the
|
||||||
|
// viewport does not inherit the real-list offset (which would be out of range).
|
||||||
func (m Model) filteredPane(pane BrowserPane) BrowserPane {
|
func (m Model) filteredPane(pane BrowserPane) BrowserPane {
|
||||||
if m.filterQuery == "" {
|
if m.filterQuery == "" {
|
||||||
return pane
|
return pane
|
||||||
}
|
}
|
||||||
query := strings.ToLower(m.filterQuery)
|
query := strings.ToLower(m.filterQuery)
|
||||||
filtered := make([]vfs.Entry, 0, len(pane.Entries))
|
filtered := make([]vfs.Entry, 0, len(pane.Entries))
|
||||||
for _, entry := range pane.Entries {
|
filteredCursor := 0
|
||||||
|
for i, entry := range pane.Entries {
|
||||||
if entry.IsParent || strings.Contains(strings.ToLower(entry.DisplayName()), query) {
|
if entry.IsParent || strings.Contains(strings.ToLower(entry.DisplayName()), query) {
|
||||||
|
if i == pane.Cursor {
|
||||||
|
filteredCursor = len(filtered)
|
||||||
|
}
|
||||||
filtered = append(filtered, entry)
|
filtered = append(filtered, entry)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pane.Entries = filtered
|
pane.Entries = filtered
|
||||||
|
pane.Cursor = filteredCursor
|
||||||
if pane.Cursor >= len(filtered) {
|
if pane.Cursor >= len(filtered) {
|
||||||
pane.Cursor = max(len(filtered)-1, 0)
|
pane.Cursor = max(len(filtered)-1, 0)
|
||||||
}
|
}
|
||||||
if pane.Offset > pane.Cursor {
|
|
||||||
pane.Offset = pane.Cursor
|
// Recompute offset in filtered-entry space. The source offset is in
|
||||||
|
// real-entry-index space and is meaningless for the shorter filtered list.
|
||||||
|
pageSize := max(m.bodyHeight()-4, 1)
|
||||||
|
offset := 0
|
||||||
|
if pane.Cursor >= pageSize {
|
||||||
|
offset = pane.Cursor - pageSize + 1
|
||||||
}
|
}
|
||||||
|
pane.Offset = offset
|
||||||
|
|
||||||
return pane
|
return pane
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1240,7 +1337,11 @@ func (m *Model) selectMoveCursor(delta int) {
|
||||||
if selected, ok := pane.Selected(); ok && !selected.IsParent {
|
if selected, ok := pane.Selected(); ok && !selected.IsParent {
|
||||||
pane.ToggleMarked(selected.Path)
|
pane.ToggleMarked(selected.Path)
|
||||||
}
|
}
|
||||||
|
if m.filterQuery != "" && m.filterPaneID == m.active {
|
||||||
|
m.moveFilteredCursor(delta)
|
||||||
|
} else {
|
||||||
pane.Move(delta, max(m.bodyHeight()-4, 1))
|
pane.Move(delta, max(m.bodyHeight()-4, 1))
|
||||||
|
}
|
||||||
m.hover = hoverState{}
|
m.hover = hoverState{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1283,6 +1384,8 @@ func (m *Model) enterSelected() error {
|
||||||
return m.goParent()
|
return m.goParent()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Save current directory to history before navigating.
|
||||||
|
pane.PushHistory(pane.Path)
|
||||||
currentName := selected.Name
|
currentName := selected.Name
|
||||||
pane.Path = selected.Path
|
pane.Path = selected.Path
|
||||||
if err := m.reloadPane(pane.ID, currentName); err != nil {
|
if err := m.reloadPane(pane.ID, currentName); err != nil {
|
||||||
|
|
@ -1297,6 +1400,7 @@ func (m *Model) clearFilter() {
|
||||||
m.filterInput.SetValue("")
|
m.filterInput.SetValue("")
|
||||||
m.filterInput.Blur()
|
m.filterInput.Blur()
|
||||||
m.filterMode = false
|
m.filterMode = false
|
||||||
|
m.filterPaneID = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) handleOpenSelected() (tea.Model, tea.Cmd) {
|
func (m *Model) handleOpenSelected() (tea.Model, tea.Cmd) {
|
||||||
|
|
@ -1306,7 +1410,6 @@ func (m *Model) handleOpenSelected() (tea.Model, tea.Cmd) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if selected.IsDir {
|
if selected.IsDir {
|
||||||
m.clearFilter()
|
|
||||||
if err := m.enterSelected(); err != nil {
|
if err := m.enterSelected(); err != nil {
|
||||||
m.status = err.Error()
|
m.status = err.Error()
|
||||||
return m, nil
|
return m, nil
|
||||||
|
|
@ -1315,7 +1418,6 @@ func (m *Model) handleOpenSelected() (tea.Model, tea.Cmd) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if isArchiveEntry(selected) {
|
if isArchiveEntry(selected) {
|
||||||
m.clearFilter()
|
|
||||||
if err := m.enterArchive(selected); err != nil {
|
if err := m.enterArchive(selected); err != nil {
|
||||||
m.status = err.Error()
|
m.status = err.Error()
|
||||||
return m, nil
|
return m, nil
|
||||||
|
|
@ -1331,7 +1433,6 @@ func (m *Model) handleOpenSelected() (tea.Model, tea.Cmd) {
|
||||||
|
|
||||||
func (m *Model) goParent() error {
|
func (m *Model) goParent() error {
|
||||||
m.hover = hoverState{}
|
m.hover = hoverState{}
|
||||||
m.clearFilter()
|
|
||||||
pane := m.activePane()
|
pane := m.activePane()
|
||||||
|
|
||||||
if mount, ok := pane.CurrentArchive(); ok {
|
if mount, ok := pane.CurrentArchive(); ok {
|
||||||
|
|
@ -1339,6 +1440,8 @@ func (m *Model) goParent() error {
|
||||||
current := filepath.Clean(pane.Path)
|
current := filepath.Clean(pane.Path)
|
||||||
if current == root {
|
if current == root {
|
||||||
// At archive root — pop archive and return to the directory containing it
|
// At archive root — pop archive and return to the directory containing it
|
||||||
|
// Save current path to history before leaving so forward can restore it.
|
||||||
|
pane.PushHistory(pane.Path)
|
||||||
if _, popped := pane.PopArchive(); popped {
|
if _, popped := pane.PopArchive(); popped {
|
||||||
_ = os.RemoveAll(mount.TempDir)
|
_ = os.RemoveAll(mount.TempDir)
|
||||||
}
|
}
|
||||||
|
|
@ -1350,6 +1453,7 @@ func (m *Model) goParent() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
// Inside archive subdirectory — go up one level within the archive
|
// Inside archive subdirectory — go up one level within the archive
|
||||||
|
pane.PushHistory(pane.Path)
|
||||||
parent := filepath.Dir(current)
|
parent := filepath.Dir(current)
|
||||||
pane.Path = parent
|
pane.Path = parent
|
||||||
if err := m.reloadPane(pane.ID, filepath.Base(current)); err != nil {
|
if err := m.reloadPane(pane.ID, filepath.Base(current)); err != nil {
|
||||||
|
|
@ -1363,6 +1467,7 @@ func (m *Model) goParent() error {
|
||||||
if parent == pane.Path {
|
if parent == pane.Path {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
pane.PushHistory(pane.Path)
|
||||||
currentName := filepath.Base(pane.Path)
|
currentName := filepath.Base(pane.Path)
|
||||||
pane.Path = parent
|
pane.Path = parent
|
||||||
if err := m.reloadPane(pane.ID, currentName); err != nil {
|
if err := m.reloadPane(pane.ID, currentName); err != nil {
|
||||||
|
|
@ -1372,6 +1477,48 @@ func (m *Model) goParent() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// historyBack navigates the active pane to the previous directory in its history.
|
||||||
|
func (m *Model) historyBack() (tea.Model, tea.Cmd) {
|
||||||
|
pane := m.activePane()
|
||||||
|
prevPath, ok := pane.PopHistory()
|
||||||
|
if !ok {
|
||||||
|
m.status = "No directory history"
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
// Save current path to forward-stack so forward navigation can restore it.
|
||||||
|
pane.PushFuture(pane.Path)
|
||||||
|
pane.Path = prevPath
|
||||||
|
pane.Cursor = 0
|
||||||
|
pane.Offset = 0
|
||||||
|
if err := m.reloadPane(pane.ID, ""); err != nil {
|
||||||
|
m.status = err.Error()
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
m.status = fmt.Sprintf("History back to %s", prevPath)
|
||||||
|
return m, m.loadPreviewCmd()
|
||||||
|
}
|
||||||
|
|
||||||
|
// historyForward navigates the active pane to the next directory in its forward-stack.
|
||||||
|
func (m *Model) historyForward() (tea.Model, tea.Cmd) {
|
||||||
|
pane := m.activePane()
|
||||||
|
nextPath, ok := pane.PopFuture()
|
||||||
|
if !ok {
|
||||||
|
m.status = "No forward history"
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
// Save current path to back-stack so back navigation can restore it.
|
||||||
|
pane.PushHistory(pane.Path)
|
||||||
|
pane.Path = nextPath
|
||||||
|
pane.Cursor = 0
|
||||||
|
pane.Offset = 0
|
||||||
|
if err := m.reloadPane(pane.ID, ""); err != nil {
|
||||||
|
m.status = err.Error()
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
m.status = fmt.Sprintf("History forward to %s", nextPath)
|
||||||
|
return m, m.loadPreviewCmd()
|
||||||
|
}
|
||||||
|
|
||||||
func (m Model) loadPreviewCmd() tea.Cmd {
|
func (m Model) loadPreviewCmd() tea.Cmd {
|
||||||
selected, ok := m.activePane().Selected()
|
selected, ok := m.activePane().Selected()
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|
@ -2013,20 +2160,20 @@ func (m *Model) openHelpModal() {
|
||||||
" Enter / Right open selected entry",
|
" Enter / Right open selected entry",
|
||||||
" Backspace/Left go to parent directory",
|
" Backspace/Left go to parent directory",
|
||||||
" Tab / h / l switch active pane",
|
" Tab / h / l switch active pane",
|
||||||
" F2 / r rename selected entry",
|
" Alt+Left directory history back",
|
||||||
|
" Alt+Right directory history forward",
|
||||||
|
" / filter entries by name in current pane",
|
||||||
" Ctrl+r refresh both panes",
|
" Ctrl+r refresh both panes",
|
||||||
"",
|
"",
|
||||||
"View and Panels",
|
"View and Panels",
|
||||||
" F9 / o toggle preview/info pane",
|
" o toggle preview/info pane",
|
||||||
" F3 plain text view or fullscreen image viewer",
|
|
||||||
" i show text caret in preview pane",
|
" i show text caret in preview pane",
|
||||||
" v start visual selection from caret",
|
" v start visual selection from caret",
|
||||||
" F3 / Esc / q close view mode",
|
" Esc / q close view/info/caret mode",
|
||||||
" yy copy current line in caret mode",
|
" yy copy current line in caret mode",
|
||||||
" y copy visual selection to clipboard",
|
" y copy visual selection to clipboard",
|
||||||
" h / l move caret left/right",
|
" h / l move caret left/right",
|
||||||
" w / b move caret by word",
|
" w / b move caret by word",
|
||||||
" q / Esc close caret/info mode",
|
|
||||||
" Ctrl+t mouse selection mode in text preview pane",
|
" Ctrl+t mouse selection mode in text preview pane",
|
||||||
" Space calculate selected directory size",
|
" Space calculate selected directory size",
|
||||||
" s cycle sort mode",
|
" s cycle sort mode",
|
||||||
|
|
@ -2034,11 +2181,11 @@ func (m *Model) openHelpModal() {
|
||||||
" t cycle theme",
|
" t cycle theme",
|
||||||
"",
|
"",
|
||||||
"Dialogs and Transfers",
|
"Dialogs and Transfers",
|
||||||
|
" r rename selected entry",
|
||||||
" Enter / y confirm action",
|
" Enter / y confirm action",
|
||||||
" Esc / n cancel action",
|
" Esc / n cancel action",
|
||||||
" b run copy/move in background (progress dialog)",
|
" b run copy/move in background (progress dialog)",
|
||||||
" c cancel active copy/move transfer",
|
" c cancel active copy/move transfer",
|
||||||
" F5/F6/F8 apply to marked entries when selection exists",
|
|
||||||
"",
|
"",
|
||||||
"Mouse",
|
"Mouse",
|
||||||
" Left click select entry and activate pane",
|
" Left click select entry and activate pane",
|
||||||
|
|
@ -2046,7 +2193,7 @@ func (m *Model) openHelpModal() {
|
||||||
" Right click toggle preview/info mode for clicked entry",
|
" Right click toggle preview/info mode for clicked entry",
|
||||||
" Wheel scroll list or preview area",
|
" Wheel scroll list or preview area",
|
||||||
"",
|
"",
|
||||||
"F-key actions are shown in the footer.",
|
"F1–F10 actions are shown in the footer.",
|
||||||
}
|
}
|
||||||
|
|
||||||
m.modal = modalState{
|
m.modal = modalState{
|
||||||
|
|
@ -3428,6 +3575,8 @@ func copyPlanCmd(kind fileOpKind, sourcePaths []string, targetDir string, overwr
|
||||||
|
|
||||||
func (m *Model) enterArchive(selected vfs.Entry) error {
|
func (m *Model) enterArchive(selected vfs.Entry) error {
|
||||||
pane := m.activePane()
|
pane := m.activePane()
|
||||||
|
// Save current path to history before opening the archive.
|
||||||
|
pane.PushHistory(pane.Path)
|
||||||
tempDir, err := vfs.ExtractArchiveToTemp(selected.Path)
|
tempDir, err := vfs.ExtractArchiveToTemp(selected.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,9 @@ type BrowserPane struct {
|
||||||
Offset int
|
Offset int
|
||||||
Marked map[string]struct{}
|
Marked map[string]struct{}
|
||||||
Archive []ArchiveMount
|
Archive []ArchiveMount
|
||||||
|
|
||||||
|
dirHistory []string
|
||||||
|
dirFuture []string
|
||||||
}
|
}
|
||||||
|
|
||||||
type ArchiveMount struct {
|
type ArchiveMount struct {
|
||||||
|
|
@ -184,6 +187,57 @@ func (p *BrowserPane) InArchive() bool {
|
||||||
return len(p.Archive) > 0
|
return len(p.Archive) > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PushHistory saves the current path to the back-stack and clears the forward-stack.
|
||||||
|
func (p *BrowserPane) PushHistory(path string) {
|
||||||
|
p.dirHistory = append(p.dirHistory, path)
|
||||||
|
p.dirFuture = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PopHistory returns the most recent path from the back-stack.
|
||||||
|
func (p *BrowserPane) PopHistory() (string, bool) {
|
||||||
|
if len(p.dirHistory) == 0 {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
path := p.dirHistory[len(p.dirHistory)-1]
|
||||||
|
p.dirHistory = p.dirHistory[:len(p.dirHistory)-1]
|
||||||
|
return path, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// PushFuture saves the current path to the forward-stack.
|
||||||
|
func (p *BrowserPane) PushFuture(path string) {
|
||||||
|
p.dirFuture = append(p.dirFuture, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PopFuture returns the most recent path from the forward-stack.
|
||||||
|
func (p *BrowserPane) PopFuture() (string, bool) {
|
||||||
|
if len(p.dirFuture) == 0 {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
path := p.dirFuture[len(p.dirFuture)-1]
|
||||||
|
p.dirFuture = p.dirFuture[:len(p.dirFuture)-1]
|
||||||
|
return path, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasHistory returns true if there are entries in the back-stack.
|
||||||
|
func (p *BrowserPane) HasHistory() bool {
|
||||||
|
return len(p.dirHistory) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasFuture returns true if there are entries in the forward-stack.
|
||||||
|
func (p *BrowserPane) HasFuture() bool {
|
||||||
|
return len(p.dirFuture) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// HistoryDepth returns the number of entries in the back-stack.
|
||||||
|
func (p *BrowserPane) HistoryDepth() int {
|
||||||
|
return len(p.dirHistory)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FutureDepth returns the number of entries in the forward-stack.
|
||||||
|
func (p *BrowserPane) FutureDepth() int {
|
||||||
|
return len(p.dirFuture)
|
||||||
|
}
|
||||||
|
|
||||||
func (p *BrowserPane) PushArchive(mount ArchiveMount) {
|
func (p *BrowserPane) PushArchive(mount ArchiveMount) {
|
||||||
p.Archive = append(p.Archive, mount)
|
p.Archive = append(p.Archive, mount)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue