feat: merge F8/x delete with F11 archive extraction
- Delete mode toggle ('d') inside existing confirm dialog instead of separate modal
- F11/e shows confirm dialog before extracting archive to opposite pane
- deleteKind field toggles between 'permanent' and 'trash'
- Active pane reloads after extraction so files appear immediately
This commit is contained in:
parent
8589187a10
commit
3229d9b263
3 changed files with 204 additions and 114 deletions
|
|
@ -44,6 +44,23 @@ func ExtractArchiveToTemp(sourcePath string) (string, error) {
|
||||||
return tempDir, nil
|
return tempDir, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ExtractArchiveToDir extracts an archive to the specified target directory.
|
||||||
|
// Unlike ExtractArchiveToTemp, it extracts directly to targetDir without
|
||||||
|
// creating a temporary directory.
|
||||||
|
func ExtractArchiveToDir(sourcePath, targetDir string) error {
|
||||||
|
sourceLower := strings.ToLower(sourcePath)
|
||||||
|
switch {
|
||||||
|
case strings.HasSuffix(sourceLower, ".zip"):
|
||||||
|
return extractZipArchive(sourcePath, targetDir)
|
||||||
|
case strings.HasSuffix(sourceLower, ".tar"):
|
||||||
|
return extractTarArchive(sourcePath, targetDir, false)
|
||||||
|
case strings.HasSuffix(sourceLower, ".tar.gz"), strings.HasSuffix(sourceLower, ".tgz"):
|
||||||
|
return extractTarArchive(sourcePath, targetDir, true)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("archive format is not supported: %s", filepath.Ext(sourcePath))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func extractZipArchive(sourcePath string, targetDir string) error {
|
func extractZipArchive(sourcePath string, targetDir string) error {
|
||||||
reader, err := zip.OpenReader(sourcePath)
|
reader, err := zip.OpenReader(sourcePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -3,95 +3,95 @@ package ui
|
||||||
import "github.com/charmbracelet/bubbles/key"
|
import "github.com/charmbracelet/bubbles/key"
|
||||||
|
|
||||||
type KeyMap struct {
|
type KeyMap struct {
|
||||||
Help key.Binding
|
Help key.Binding
|
||||||
Visual key.Binding
|
Visual key.Binding
|
||||||
Caret key.Binding
|
Caret key.Binding
|
||||||
View key.Binding
|
View key.Binding
|
||||||
Edit key.Binding
|
Edit key.Binding
|
||||||
Rename key.Binding
|
Rename key.Binding
|
||||||
Info key.Binding
|
Info key.Binding
|
||||||
Archive key.Binding
|
Archive key.Binding
|
||||||
SelectText key.Binding
|
SelectText key.Binding
|
||||||
ToggleHidden key.Binding
|
ToggleHidden key.Binding
|
||||||
CycleTheme key.Binding
|
CycleTheme key.Binding
|
||||||
CycleSort key.Binding
|
CycleSort key.Binding
|
||||||
SSH key.Binding
|
SSH key.Binding
|
||||||
Up key.Binding
|
Up key.Binding
|
||||||
Down key.Binding
|
Down key.Binding
|
||||||
SelectUp key.Binding
|
SelectUp key.Binding
|
||||||
SelectDown key.Binding
|
SelectDown key.Binding
|
||||||
PageUp key.Binding
|
PageUp key.Binding
|
||||||
PageDown key.Binding
|
PageDown key.Binding
|
||||||
Open key.Binding
|
Open key.Binding
|
||||||
Back key.Binding
|
Back key.Binding
|
||||||
Switch key.Binding
|
Switch key.Binding
|
||||||
Filter key.Binding
|
Filter key.Binding
|
||||||
Refresh key.Binding
|
Refresh key.Binding
|
||||||
DirSize key.Binding
|
DirSize key.Binding
|
||||||
Copy key.Binding
|
Copy key.Binding
|
||||||
Move key.Binding
|
Move key.Binding
|
||||||
Mkdir key.Binding
|
Mkdir key.Binding
|
||||||
Delete key.Binding
|
Delete key.Binding
|
||||||
PermanentDelete key.Binding
|
ExtractArchive key.Binding
|
||||||
Confirm key.Binding
|
Confirm key.Binding
|
||||||
Background key.Binding
|
Background key.Binding
|
||||||
ProgressCancel key.Binding
|
ProgressCancel key.Binding
|
||||||
Cancel key.Binding
|
Cancel key.Binding
|
||||||
Quit key.Binding
|
Quit key.Binding
|
||||||
HistoryBack key.Binding
|
HistoryBack key.Binding
|
||||||
HistoryForward key.Binding
|
HistoryForward key.Binding
|
||||||
}
|
}
|
||||||
|
|
||||||
func DefaultKeyMap() KeyMap {
|
func DefaultKeyMap() KeyMap {
|
||||||
return KeyMap{
|
return KeyMap{
|
||||||
Help: key.NewBinding(key.WithKeys("f1", "?"), key.WithHelp("F1/?", "help")),
|
Help: key.NewBinding(key.WithKeys("f1", "?"), key.WithHelp("F1/?", "help")),
|
||||||
Rename: key.NewBinding(key.WithKeys("f2", "r"), key.WithHelp("F2/r", "rename")),
|
Rename: key.NewBinding(key.WithKeys("f2", "r"), key.WithHelp("F2/r", "rename")),
|
||||||
View: key.NewBinding(key.WithKeys("f3", "v"), key.WithHelp("F3/v", "view")),
|
View: key.NewBinding(key.WithKeys("f3", "v"), key.WithHelp("F3/v", "view")),
|
||||||
Visual: key.NewBinding(key.WithKeys("v"), key.WithHelp("v", "visual")),
|
Visual: key.NewBinding(key.WithKeys("v"), key.WithHelp("v", "visual")),
|
||||||
Caret: key.NewBinding(key.WithKeys("i"), key.WithHelp("i", "caret")),
|
Caret: key.NewBinding(key.WithKeys("i"), key.WithHelp("i", "caret")),
|
||||||
Edit: key.NewBinding(key.WithKeys("e"), key.WithHelp("e", "edit")),
|
Edit: key.NewBinding(key.WithKeys("e"), key.WithHelp("e", "edit")),
|
||||||
Archive: key.NewBinding(key.WithKeys("f4", "a"), key.WithHelp("F4/a", "archive")),
|
Archive: key.NewBinding(key.WithKeys("f4", "a"), key.WithHelp("F4/a", "archive")),
|
||||||
Info: key.NewBinding(key.WithKeys("f9", "o"), key.WithHelp("F9/o", "info")),
|
Info: key.NewBinding(key.WithKeys("f9", "o"), key.WithHelp("F9/o", "info")),
|
||||||
SelectText: key.NewBinding(key.WithKeys("ctrl+t"), key.WithHelp("C-t", "text select")),
|
SelectText: key.NewBinding(key.WithKeys("ctrl+t"), key.WithHelp("C-t", "text select")),
|
||||||
ToggleHidden: key.NewBinding(key.WithKeys("."), key.WithHelp(".", "hidden")),
|
ToggleHidden: key.NewBinding(key.WithKeys("."), key.WithHelp(".", "hidden")),
|
||||||
CycleTheme: key.NewBinding(key.WithKeys("t"), key.WithHelp("t", "theme")),
|
CycleTheme: key.NewBinding(key.WithKeys("t"), key.WithHelp("t", "theme")),
|
||||||
CycleSort: key.NewBinding(key.WithKeys("g"), key.WithHelp("g", "sort")),
|
CycleSort: key.NewBinding(key.WithKeys("g"), key.WithHelp("g", "sort")),
|
||||||
SSH: key.NewBinding(key.WithKeys("f12", "s"), key.WithHelp("F12/s", "ssh")),
|
SSH: key.NewBinding(key.WithKeys("f12", "s"), key.WithHelp("F12/s", "ssh")),
|
||||||
Up: key.NewBinding(key.WithKeys("up", "k"), key.WithHelp("↑/k", "up")),
|
Up: key.NewBinding(key.WithKeys("up", "k"), key.WithHelp("↑/k", "up")),
|
||||||
Down: key.NewBinding(key.WithKeys("down", "j"), key.WithHelp("↓/j", "down")),
|
Down: key.NewBinding(key.WithKeys("down", "j"), key.WithHelp("↓/j", "down")),
|
||||||
SelectUp: key.NewBinding(key.WithKeys("shift+up", "K"), key.WithHelp("S-↑/K", "select up")),
|
SelectUp: key.NewBinding(key.WithKeys("shift+up", "K"), key.WithHelp("S-↑/K", "select up")),
|
||||||
SelectDown: key.NewBinding(key.WithKeys("shift+down", "J"), key.WithHelp("S-↓/J", "select down")),
|
SelectDown: key.NewBinding(key.WithKeys("shift+down", "J"), key.WithHelp("S-↓/J", "select down")),
|
||||||
PageUp: key.NewBinding(key.WithKeys("pgup"), key.WithHelp("PgUp", "page up")),
|
PageUp: key.NewBinding(key.WithKeys("pgup"), key.WithHelp("PgUp", "page up")),
|
||||||
PageDown: key.NewBinding(),
|
PageDown: key.NewBinding(),
|
||||||
Filter: key.NewBinding(key.WithKeys("/"), key.WithHelp("/", "filter")),
|
Filter: key.NewBinding(key.WithKeys("/"), key.WithHelp("/", "filter")),
|
||||||
Open: key.NewBinding(key.WithKeys("enter", "right"), key.WithHelp("Enter", "open")),
|
Open: key.NewBinding(key.WithKeys("enter", "right"), key.WithHelp("Enter", "open")),
|
||||||
Back: key.NewBinding(key.WithKeys("backspace", "left"), key.WithHelp("←", "parent")),
|
Back: key.NewBinding(key.WithKeys("backspace", "left"), key.WithHelp("←", "parent")),
|
||||||
Switch: key.NewBinding(key.WithKeys("tab", "h", "l"), key.WithHelp("Tab/h/l", "switch pane")),
|
Switch: key.NewBinding(key.WithKeys("tab", "h", "l"), key.WithHelp("Tab/h/l", "switch pane")),
|
||||||
Refresh: key.NewBinding(key.WithKeys("ctrl+r"), key.WithHelp("C-r", "refresh")),
|
Refresh: key.NewBinding(key.WithKeys("ctrl+r"), key.WithHelp("C-r", "refresh")),
|
||||||
DirSize: key.NewBinding(key.WithKeys(" "), key.WithHelp("Space", "dir size")),
|
DirSize: key.NewBinding(key.WithKeys(" "), key.WithHelp("Space", "dir size")),
|
||||||
Copy: key.NewBinding(key.WithKeys("f5", "c"), key.WithHelp("F5/c", "copy")),
|
Copy: key.NewBinding(key.WithKeys("f5", "c"), key.WithHelp("F5/c", "copy")),
|
||||||
Move: key.NewBinding(key.WithKeys("f6", "m"), key.WithHelp("F6/m", "move")),
|
Move: key.NewBinding(key.WithKeys("f6", "m"), key.WithHelp("F6/m", "move")),
|
||||||
Mkdir: key.NewBinding(key.WithKeys("f7", "n"), key.WithHelp("F7/n", "mkdir")),
|
Mkdir: key.NewBinding(key.WithKeys("f7", "n"), key.WithHelp("F7/n", "mkdir")),
|
||||||
Delete: key.NewBinding(key.WithKeys("f8", "delete", "x"), key.WithHelp("F8/x", "trash")),
|
Delete: key.NewBinding(key.WithKeys("f8", "delete", "x"), key.WithHelp("F8/x", "delete")),
|
||||||
PermanentDelete: key.NewBinding(key.WithKeys("f11", "d"), key.WithHelp("F11/d", "permanent delete")),
|
ExtractArchive: key.NewBinding(key.WithKeys("f11", "e"), key.WithHelp("F11/e", "extract archive")),
|
||||||
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")),
|
HistoryBack: key.NewBinding(key.WithKeys("alt+left"), key.WithHelp("A-←", "back")),
|
||||||
HistoryForward: key.NewBinding(key.WithKeys("alt+right"), key.WithHelp("A-→", "forward")),
|
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")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k KeyMap) ShortHelp() []key.Binding {
|
func (k KeyMap) ShortHelp() []key.Binding {
|
||||||
return []key.Binding{k.Help, k.Rename, k.View, k.Archive, k.Copy, k.Move, k.Mkdir, k.Delete, k.Info, k.Quit, k.PermanentDelete, k.SSH}
|
return []key.Binding{k.Help, k.Rename, k.View, k.Archive, k.Copy, k.Move, k.Mkdir, k.Delete, k.Info, k.Quit, k.ExtractArchive, k.SSH}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k KeyMap) FullHelp() [][]key.Binding {
|
func (k KeyMap) FullHelp() [][]key.Binding {
|
||||||
return [][]key.Binding{
|
return [][]key.Binding{
|
||||||
{k.Help, k.Up, k.Down, k.SelectUp, k.SelectDown, k.Open, k.Back},
|
{k.Help, k.Up, k.Down, k.SelectUp, k.SelectDown, k.Open, k.Back},
|
||||||
{k.Rename, k.View, k.Caret, k.Edit, k.Archive, k.Copy, k.Move, k.Delete},
|
{k.Rename, k.View, k.Caret, k.Edit, k.Archive, k.Copy, k.Move, k.Delete},
|
||||||
{k.PermanentDelete, k.SelectText, k.DirSize, k.Refresh, k.ToggleHidden, k.CycleSort, k.CycleTheme, k.Quit},
|
{k.ExtractArchive, k.SelectText, k.DirSize, k.Refresh, k.ToggleHidden, k.CycleSort, k.CycleTheme, k.Quit},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,7 @@ const (
|
||||||
opArchive
|
opArchive
|
||||||
opExecute
|
opExecute
|
||||||
opDeleteHost
|
opDeleteHost
|
||||||
|
opExtractArchive
|
||||||
)
|
)
|
||||||
|
|
||||||
type pendingOperation struct {
|
type pendingOperation struct {
|
||||||
|
|
@ -248,6 +249,7 @@ type Model struct {
|
||||||
nextArchiveJob int
|
nextArchiveJob int
|
||||||
archiveProgress chan tea.Msg
|
archiveProgress chan tea.Msg
|
||||||
archiveFormat string
|
archiveFormat string
|
||||||
|
deleteKind string // "trash" or "permanent" — selected in delete modal
|
||||||
ssh *sshState
|
ssh *sshState
|
||||||
preSSHPath string // original path before entering SSH mode
|
preSSHPath string // original path before entering SSH mode
|
||||||
}
|
}
|
||||||
|
|
@ -511,6 +513,21 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
case opExecute:
|
case opExecute:
|
||||||
m.status = "Executable closed"
|
m.status = "Executable closed"
|
||||||
return m, tea.Batch(m.loadPreviewCmd(), enableMouseCmd())
|
return m, tea.Batch(m.loadPreviewCmd(), enableMouseCmd())
|
||||||
|
case opExtractArchive:
|
||||||
|
if msg.err != nil {
|
||||||
|
log.Printf("[ERROR] opMsg: ExtractArchive failed — err=%v", msg.err)
|
||||||
|
m.status = msg.err.Error()
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
log.Printf("[ACTION] opMsg: ExtractArchive done — source=%s target=%s", msg.sourcePath, msg.targetPath)
|
||||||
|
m.status = fmt.Sprintf("Extracted to %s", msg.targetPath)
|
||||||
|
// Reload the target (passive) pane so extracted files appear
|
||||||
|
targetID := PaneRight
|
||||||
|
if m.active == PaneRight {
|
||||||
|
targetID = PaneLeft
|
||||||
|
}
|
||||||
|
_ = m.reloadPane(targetID, "")
|
||||||
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reload panes — use remote reload for remote mounts
|
// Reload panes — use remote reload for remote mounts
|
||||||
|
|
@ -593,20 +610,24 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
msg.kind, len(msg.sourcePaths), msg.stats.FilesTotal, msg.stats.BytesTotal, msg.srcClient != nil)
|
msg.kind, len(msg.sourcePaths), msg.stats.FilesTotal, msg.stats.BytesTotal, msg.srcClient != nil)
|
||||||
|
|
||||||
title := "Move selected entr" + pluralSuffix(len(msg.sourcePaths), "y", "ies") + " to trash?"
|
title := "Move selected entr" + pluralSuffix(len(msg.sourcePaths), "y", "ies") + " to trash?"
|
||||||
if msg.kind == opPermanentDelete {
|
if msg.srcClient != nil {
|
||||||
title = "Permanently delete selected entr" + pluralSuffix(len(msg.sourcePaths), "y", "ies") + "?"
|
|
||||||
} else if msg.srcClient != nil {
|
|
||||||
title = "Delete selected entr" + pluralSuffix(len(msg.sourcePaths), "y", "ies") + " from remote?"
|
title = "Delete selected entr" + pluralSuffix(len(msg.sourcePaths), "y", "ies") + " from remote?"
|
||||||
|
} else if m.deleteKind == "permanent" {
|
||||||
|
title = "Permanently delete selected entr" + pluralSuffix(len(msg.sourcePaths), "y", "ies") + "?"
|
||||||
}
|
}
|
||||||
bodyLines := []string{
|
bodyLines := []string{
|
||||||
fmt.Sprintf("Items: %d", len(msg.sourcePaths)),
|
fmt.Sprintf("Items: %d", len(msg.sourcePaths)),
|
||||||
fmt.Sprintf("Files: %d", msg.stats.FilesTotal),
|
fmt.Sprintf("Files: %d", msg.stats.FilesTotal),
|
||||||
fmt.Sprintf("Size: %s", formatSize(msg.stats.BytesTotal, true)),
|
fmt.Sprintf("Size: %s", formatSize(msg.stats.BytesTotal, true)),
|
||||||
}
|
}
|
||||||
|
note := "confirm-actions"
|
||||||
|
if msg.srcClient == nil {
|
||||||
|
note = fmt.Sprintf("Mode: %s (d to change)\nEnter / y to confirm, Esc / n to cancel", m.deleteKind)
|
||||||
|
}
|
||||||
m.openConfirmModal(
|
m.openConfirmModal(
|
||||||
title,
|
title,
|
||||||
strings.Join(bodyLines, "\n"),
|
strings.Join(bodyLines, "\n"),
|
||||||
"confirm-actions",
|
note,
|
||||||
pendingOperation{
|
pendingOperation{
|
||||||
kind: msg.kind,
|
kind: msg.kind,
|
||||||
sourcePaths: append([]string(nil), msg.sourcePaths...),
|
sourcePaths: append([]string(nil), msg.sourcePaths...),
|
||||||
|
|
@ -1177,8 +1198,8 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
return m, nil
|
return m, nil
|
||||||
case key.Matches(msg, m.keys.Delete):
|
case key.Matches(msg, m.keys.Delete):
|
||||||
return m.handleDelete()
|
return m.handleDelete()
|
||||||
case key.Matches(msg, m.keys.PermanentDelete):
|
case key.Matches(msg, m.keys.ExtractArchive):
|
||||||
return m.handlePermanentDelete()
|
return m.handleExtractArchive()
|
||||||
case key.Matches(msg, m.keys.SSH):
|
case key.Matches(msg, m.keys.SSH):
|
||||||
log.Printf("[KEY] SSH toggle — active=%s path=%s", m.active, activePane.Path)
|
log.Printf("[KEY] SSH toggle — active=%s path=%s", m.active, activePane.Path)
|
||||||
return m.handleSSHToggle()
|
return m.handleSSHToggle()
|
||||||
|
|
@ -1386,9 +1407,48 @@ func (m Model) handleModalKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||||
return m, m.remoteDeleteCmd(pending.sourcePaths, pending.srcClient)
|
return m, m.remoteDeleteCmd(pending.sourcePaths, pending.srcClient)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Adjust delete kind based on mode toggle
|
||||||
|
if pending.kind == opDelete || pending.kind == opPermanentDelete {
|
||||||
|
if m.deleteKind == "permanent" && pending.kind == opDelete {
|
||||||
|
pending.kind = opPermanentDelete
|
||||||
|
} else if m.deleteKind == "trash" && pending.kind == opPermanentDelete {
|
||||||
|
pending.kind = opDelete
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract archive
|
||||||
|
if pending.kind == opExtractArchive {
|
||||||
|
m.busy = true
|
||||||
|
log.Printf("[MODAL] Confirm — extract archive source=%s target=%s", pending.sourcePaths[0], pending.targetDir)
|
||||||
|
return m, extractArchiveCmd(pending.sourcePaths[0], pending.targetDir)
|
||||||
|
}
|
||||||
|
|
||||||
m.busy = true
|
m.busy = true
|
||||||
log.Printf("[MODAL] Confirm — %s sources=%d", operationVerb(pending.kind), len(pending.sourcePaths))
|
log.Printf("[MODAL] Confirm — %s sources=%d", operationVerb(pending.kind), len(pending.sourcePaths))
|
||||||
return m, pending.cmd()
|
return m, pending.cmd()
|
||||||
|
case msg.String() == "d":
|
||||||
|
if m.modal.pending == nil {
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
pkind := m.modal.pending.kind
|
||||||
|
if (pkind == opDelete || pkind == opPermanentDelete) && m.modal.pending.srcClient == nil {
|
||||||
|
if m.deleteKind == "permanent" {
|
||||||
|
m.deleteKind = "trash"
|
||||||
|
} else {
|
||||||
|
m.deleteKind = "permanent"
|
||||||
|
}
|
||||||
|
sources := m.modal.pending.sourcePaths
|
||||||
|
if m.deleteKind == "permanent" {
|
||||||
|
m.modal.title = "Permanently delete selected entr" + pluralSuffix(len(sources), "y", "ies") + "?"
|
||||||
|
} else {
|
||||||
|
m.modal.title = "Move selected entr" + pluralSuffix(len(sources), "y", "ies") + " to trash?"
|
||||||
|
}
|
||||||
|
m.modal.note = fmt.Sprintf(
|
||||||
|
"Mode: %s (d to change)\nEnter / y to confirm, Esc / n to cancel",
|
||||||
|
m.deleteKind,
|
||||||
|
)
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
case modalArchiveType:
|
case modalArchiveType:
|
||||||
|
|
@ -2188,7 +2248,7 @@ func (m *Model) handleDelete() (tea.Model, tea.Cmd) {
|
||||||
|
|
||||||
log.Printf("[ACTION] Delete: sources=%v remote=%v", sources, m.activePane().InRemote())
|
log.Printf("[ACTION] Delete: sources=%v remote=%v", sources, m.activePane().InRemote())
|
||||||
|
|
||||||
// Remote delete via SFTP (no trash on remote)
|
// Remote delete via SFTP (no trash on remote — always permanent)
|
||||||
if mount, ok := m.activePane().CurrentRemote(); ok {
|
if mount, ok := m.activePane().CurrentRemote(); ok {
|
||||||
log.Printf("[ACTION] Delete: remote — sources=%v", sources)
|
log.Printf("[ACTION] Delete: remote — sources=%v", sources)
|
||||||
m.busy = true
|
m.busy = true
|
||||||
|
|
@ -2196,47 +2256,53 @@ func (m *Model) handleDelete() (tea.Model, tea.Cmd) {
|
||||||
return m, m.remoteDeletePlanCmd(sources, mount.Client)
|
return m, m.remoteDeletePlanCmd(sources, mount.Client)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Default to permanent delete
|
||||||
|
m.deleteKind = "permanent"
|
||||||
|
|
||||||
if !m.cfg.Behavior.ConfirmDelete {
|
if !m.cfg.Behavior.ConfirmDelete {
|
||||||
log.Printf("[ACTION] Delete: immediate trash — sources=%v", sources)
|
// No plan needed — show a simple confirm dialog with mode toggle
|
||||||
m.busy = true
|
title := "Permanently delete selected entr" + pluralSuffix(len(sources), "y", "ies") + "?"
|
||||||
m.status = fmt.Sprintf("Moving %d entr%s to trash", len(sources), pluralSuffix(len(sources), "y", "ies"))
|
body := fmt.Sprintf("Items: %d", len(sources))
|
||||||
return m, trashPathsCmd(sources)
|
note := fmt.Sprintf("Mode: %s (d to change)\nEnter / y to confirm, Esc / n to cancel", m.deleteKind)
|
||||||
|
pending := pendingOperation{
|
||||||
|
kind: opPermanentDelete,
|
||||||
|
sourcePaths: append([]string(nil), sources...),
|
||||||
|
}
|
||||||
|
m.openConfirmModal(title, body, note, pending)
|
||||||
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("[ACTION] Delete: plan — sources=%v", sources)
|
// Compute plan first, then show confirm modal with mode toggle
|
||||||
m.busy = true
|
m.busy = true
|
||||||
m.status = "Calculating trash size"
|
m.status = "Calculating delete size"
|
||||||
return m, trashPlanCmd(sources)
|
return m, trashPlanCmd(sources)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) handlePermanentDelete() (tea.Model, tea.Cmd) {
|
func (m *Model) handleExtractArchive() (tea.Model, tea.Cmd) {
|
||||||
if m.activePane().InArchive() {
|
selected, ok := m.activePane().Selected()
|
||||||
log.Printf("[SKIP] PermanentDelete: archive read-only")
|
if !ok || !isArchiveEntry(selected) {
|
||||||
m.status = "Archive mode is read-only; permanent delete is disabled"
|
log.Printf("[SKIP] ExtractArchive: no archive selected")
|
||||||
|
m.status = "Select an archive file to extract"
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SSH host list — delete user-added hosts (same as F8)
|
// Target is the opposite pane's current directory
|
||||||
if m.activePane().Path == "ssh://" {
|
targetPane := m.passivePane()
|
||||||
return m.handleSSHHostDelete()
|
targetDir := targetPane.Path
|
||||||
}
|
|
||||||
|
|
||||||
sources := m.operationSources()
|
log.Printf("[ACTION] ExtractArchive: source=%s target=%s", selected.Path, targetDir)
|
||||||
if len(sources) == 0 {
|
|
||||||
log.Printf("[SKIP] PermanentDelete: no sources")
|
|
||||||
m.status = "Nothing to delete"
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
log.Printf("[ACTION] PermanentDelete: sources=%v confirm=%v", sources, m.cfg.Behavior.ConfirmDelete)
|
|
||||||
if !m.cfg.Behavior.ConfirmDelete {
|
|
||||||
m.busy = true
|
|
||||||
m.status = fmt.Sprintf("Permanently deleting %d entr%s", len(sources), pluralSuffix(len(sources), "y", "ies"))
|
|
||||||
return m, deletePathsPermanentCmd(sources)
|
|
||||||
}
|
|
||||||
|
|
||||||
m.busy = true
|
// Show confirm dialog before extracting
|
||||||
m.status = "Calculating delete size"
|
title := fmt.Sprintf("Extract %s?", selected.DisplayName())
|
||||||
return m, deletePlanPermanentCmd(sources)
|
body := fmt.Sprintf("Archive: %s\nTarget: %s", selected.Path, targetDir)
|
||||||
|
note := "Enter / y to confirm, Esc / n to cancel"
|
||||||
|
pending := pendingOperation{
|
||||||
|
kind: opExtractArchive,
|
||||||
|
sourcePaths: []string{selected.Path},
|
||||||
|
targetDir: targetDir,
|
||||||
|
}
|
||||||
|
m.openConfirmModal(title, body, note, pending)
|
||||||
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) handleView() (tea.Model, tea.Cmd) {
|
func (m *Model) handleView() (tea.Model, tea.Cmd) {
|
||||||
|
|
@ -2794,8 +2860,8 @@ func (m *Model) openHelpModal() {
|
||||||
" t cycle theme",
|
" t cycle theme",
|
||||||
"",
|
"",
|
||||||
"Dialogs and Transfers",
|
"Dialogs and Transfers",
|
||||||
" F8 / x move selected entry to trash",
|
" F8 / x delete (trash or permanent, d to toggle)",
|
||||||
" F11 / d permanently delete selected entry",
|
" F11 extract archive to opposite pane",
|
||||||
" r rename selected entry",
|
" r rename selected entry",
|
||||||
" Enter / y confirm action",
|
" Enter / y confirm action",
|
||||||
" Esc / n cancel action",
|
" Esc / n cancel action",
|
||||||
|
|
@ -4603,6 +4669,13 @@ func deletePlanPermanentCmd(sourcePaths []string) tea.Cmd {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func extractArchiveCmd(sourcePath, targetDir string) tea.Cmd {
|
||||||
|
return func() tea.Msg {
|
||||||
|
err := vfs.ExtractArchiveToDir(sourcePath, targetDir)
|
||||||
|
return opMsg{kind: opExtractArchive, sourcePath: sourcePath, targetPath: targetDir, err: err}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (m *Model) mkdirCmd(parent, name string) tea.Cmd {
|
func (m *Model) mkdirCmd(parent, name string) tea.Cmd {
|
||||||
// Remote mkdir via SFTP
|
// Remote mkdir via SFTP
|
||||||
if mount, ok := m.activePane().CurrentRemote(); ok {
|
if mount, ok := m.activePane().CurrentRemote(); ok {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue