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:
vrubelroman 2026-04-29 13:42:59 +03:00
parent 8589187a10
commit 3229d9b263
3 changed files with 204 additions and 114 deletions

View file

@ -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 {

View file

@ -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},
} }
} }

View file

@ -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 {