fix: cursor position on Enter for '..' now lands on source folder (same as Backspace); feat: permanent delete via F11/d; fix: footer F-key order (F1-F11)

This commit is contained in:
vrubelroman 2026-04-27 18:56:20 +03:00
parent 7a55fb289e
commit 813c40a41e
3 changed files with 253 additions and 114 deletions

View file

@ -262,6 +262,65 @@ func DeletePath(path string) error {
return os.RemoveAll(path) return os.RemoveAll(path)
} }
// MoveToTrash moves a file or directory to the FreeDesktop Trash directory
// (~/.local/share/Trash). Follows the FreeDesktop Trash specification:
// - The original item is moved to Trash/files/<basename>
// - A .trashinfo file is written to Trash/info/<basename>.trashinfo
// - If <basename> already exists in Trash/files, a numeric suffix is appended.
func MoveToTrash(path string) error {
home, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("cannot determine home directory: %w", err)
}
trashDir := filepath.Join(home, ".local", "share", "Trash")
filesDir := filepath.Join(trashDir, "files")
infoDir := filepath.Join(trashDir, "info")
if err := os.MkdirAll(filesDir, 0o700); err != nil {
return fmt.Errorf("cannot create trash files directory: %w", err)
}
if err := os.MkdirAll(infoDir, 0o700); err != nil {
return fmt.Errorf("cannot create trash info directory: %w", err)
}
baseName := filepath.Base(path)
// Generate a unique name in the trash directory
destName := baseName
for counter := 1; ; counter++ {
destPath := filepath.Join(filesDir, destName)
if _, err := os.Stat(destPath); os.IsNotExist(err) {
break
} else if err != nil {
return fmt.Errorf("cannot stat trash path: %w", err)
}
destName = fmt.Sprintf("%s.%d", baseName, counter)
}
destPath := filepath.Join(filesDir, destName)
if err := os.Rename(path, destPath); err != nil {
// Cross-filesystem move: fall back to copy+delete
return fmt.Errorf("cannot move to trash: %w", err)
}
// Write .trashinfo file
absPath, err := filepath.Abs(path)
if err != nil {
absPath = path
}
now := time.Now().Format("2006-01-02T15:04:05")
infoContent := fmt.Sprintf("[Trash Info]\nPath=%s\nDeletionDate=%s\n", absPath, now)
infoPath := filepath.Join(infoDir, destName+".trashinfo")
if err := os.WriteFile(infoPath, []byte(infoContent), 0o600); err != nil {
// Best-effort: if info file fails, try to move the file back
_ = os.Rename(destPath, path)
return fmt.Errorf("cannot write trash info: %w", err)
}
return nil
}
func MakeDir(parent string, name string) (string, error) { func MakeDir(parent string, name string) (string, error) {
target := filepath.Join(parent, name) target := filepath.Join(parent, name)
if err := os.MkdirAll(target, 0o755); err != nil { if err := os.MkdirAll(target, 0o755); err != nil {

View file

@ -3,91 +3,93 @@ 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
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
Confirm key.Binding PermanentDelete key.Binding
Background key.Binding Confirm key.Binding
ProgressCancel key.Binding Background key.Binding
Cancel key.Binding ProgressCancel key.Binding
Quit key.Binding Cancel key.Binding
HistoryBack key.Binding Quit key.Binding
HistoryForward key.Binding HistoryBack 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("s"), key.WithHelp("s", "sort")), CycleSort: key.NewBinding(key.WithKeys("s"), key.WithHelp("s", "sort")),
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", "delete")), Delete: key.NewBinding(key.WithKeys("f8", "delete", "x"), key.WithHelp("F8/x", "trash")),
Confirm: key.NewBinding(key.WithKeys("enter", "y"), key.WithHelp("Enter/y", "confirm")), PermanentDelete: key.NewBinding(key.WithKeys("f11", "d"), key.WithHelp("F11/d", "permanent delete")),
Background: key.NewBinding(key.WithKeys("b"), key.WithHelp("b", "background")), Confirm: key.NewBinding(key.WithKeys("enter", "y"), key.WithHelp("Enter/y", "confirm")),
ProgressCancel: key.NewBinding(key.WithKeys("c"), key.WithHelp("c", "cancel transfer")), Background: key.NewBinding(key.WithKeys("b"), key.WithHelp("b", "background")),
HistoryBack: key.NewBinding(key.WithKeys("alt+left"), key.WithHelp("A-←", "back")), ProgressCancel: key.NewBinding(key.WithKeys("c"), key.WithHelp("c", "cancel transfer")),
HistoryForward: key.NewBinding(key.WithKeys("alt+right"), key.WithHelp("A-→", "forward")), HistoryBack: key.NewBinding(key.WithKeys("alt+left"), key.WithHelp("A-←", "back")),
Cancel: key.NewBinding(key.WithKeys("esc"), key.WithHelp("Esc", "cancel")), HistoryForward: key.NewBinding(key.WithKeys("alt+right"), key.WithHelp("A-→", "forward")),
Quit: key.NewBinding(key.WithKeys("f10", "q", "ctrl+c"), key.WithHelp("F10/q", "quit")), Cancel: key.NewBinding(key.WithKeys("esc"), key.WithHelp("Esc", "cancel")),
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} 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}
} }
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.SelectText, k.DirSize, k.Refresh, k.ToggleHidden, k.CycleSort, k.CycleTheme, k.Quit}, {k.PermanentDelete, k.SelectText, k.DirSize, k.Refresh, k.ToggleHidden, k.CycleSort, k.CycleTheme, k.Quit},
} }
} }

View file

@ -45,6 +45,7 @@ const (
opCopy fileOpKind = iota opCopy fileOpKind = iota
opMove opMove
opDelete opDelete
opPermanentDelete
opMkdir opMkdir
opRename opRename
opEdit opEdit
@ -104,6 +105,7 @@ type copyProgressMsg struct {
} }
type deletePlanMsg struct { type deletePlanMsg struct {
kind fileOpKind
sourcePaths []string sourcePaths []string
stats vfs.TransferStats stats vfs.TransferStats
err error err error
@ -336,7 +338,10 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case opMove: case opMove:
m.status = fmt.Sprintf("Moved to %s", msg.targetPath) m.status = fmt.Sprintf("Moved to %s", msg.targetPath)
case opDelete: case opDelete:
m.status = "Deleted" m.status = "Moved to trash"
m.activePane().ClearMarks()
case opPermanentDelete:
m.status = "Permanently deleted"
m.activePane().ClearMarks() m.activePane().ClearMarks()
case opMkdir: case opMkdir:
m.status = fmt.Sprintf("Created %s", msg.targetPath) m.status = fmt.Sprintf("Created %s", msg.targetPath)
@ -396,7 +401,10 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, nil return m, nil
} }
title := "Delete selected entr" + pluralSuffix(len(msg.sourcePaths), "y", "ies") + "?" title := "Move selected entr" + pluralSuffix(len(msg.sourcePaths), "y", "ies") + " to trash?"
if msg.kind == opPermanentDelete {
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),
@ -407,7 +415,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
strings.Join(bodyLines, "\n"), strings.Join(bodyLines, "\n"),
"confirm-actions", "confirm-actions",
pendingOperation{ pendingOperation{
kind: opDelete, kind: msg.kind,
sourcePaths: append([]string(nil), msg.sourcePaths...), sourcePaths: append([]string(nil), msg.sourcePaths...),
stats: msg.stats, stats: msg.stats,
}, },
@ -893,6 +901,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):
return m.handlePermanentDelete()
} }
case tea.MouseMsg: case tea.MouseMsg:
@ -1376,19 +1386,10 @@ func (m *Model) enterSelected() error {
m.status = "File is shown in the middle pane. Use F3 for pager or F4 for editor." m.status = "File is shown in the middle pane. Use F3 for pager or F4 for editor."
return nil return nil
} }
// When inside an archive mount and selecting "..", use archive-aware
// navigation (goParent) instead of blindly setting pane.Path to the
// parent directory (which would be /tmp for a temp-mounted archive).
if selected.IsParent {
if _, archiveMounted := pane.CurrentArchive(); archiveMounted {
return m.goParent()
}
}
// Save current directory to history before navigating. // Save current directory to history before navigating.
pane.PushHistory(pane.Path) pane.PushHistory(pane.Path)
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, selected.Name); err != nil {
return err return err
} }
m.status = fmt.Sprintf("Entered %s", pane.Path) m.status = fmt.Sprintf("Entered %s", pane.Path)
@ -1409,6 +1410,19 @@ func (m *Model) handleOpenSelected() (tea.Model, tea.Cmd) {
return m, nil return m, nil
} }
// Navigating up via ".." — use goParent which preserves the cursor
// position on the directory/archive we came from (by finding its name
// in the parent listing via FindSelected). This applies both inside
// archive mounts (where pane.Path must stay within the temp mount)
// and regular directories (for consistent cursor placement).
if selected.IsParent {
if err := m.goParent(); err != nil {
m.status = err.Error()
return m, nil
}
return m, m.loadPreviewCmd()
}
if selected.IsDir { if selected.IsDir {
if err := m.enterSelected(); err != nil { if err := m.enterSelected(); err != nil {
m.status = err.Error() m.status = err.Error()
@ -1647,13 +1661,35 @@ func (m *Model) handleDelete() (tea.Model, tea.Cmd) {
} }
if !m.cfg.Behavior.ConfirmDelete { if !m.cfg.Behavior.ConfirmDelete {
m.busy = true m.busy = true
m.status = fmt.Sprintf("Deleting %d entr%s", len(sources), pluralSuffix(len(sources), "y", "ies")) m.status = fmt.Sprintf("Moving %d entr%s to trash", len(sources), pluralSuffix(len(sources), "y", "ies"))
return m, deletePathsCmd(sources) return m, trashPathsCmd(sources)
}
m.busy = true
m.status = "Calculating trash size"
return m, trashPlanCmd(sources)
}
func (m *Model) handlePermanentDelete() (tea.Model, tea.Cmd) {
if m.activePane().InArchive() {
m.status = "Archive mode is read-only; permanent delete is disabled"
return m, nil
}
sources := m.operationSources()
if len(sources) == 0 {
m.status = "Nothing to delete"
return m, nil
}
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 m.busy = true
m.status = "Calculating delete size" m.status = "Calculating delete size"
return m, deletePlanCmd(sources) return m, deletePlanPermanentCmd(sources)
} }
func (m *Model) handleView() (tea.Model, tea.Cmd) { func (m *Model) handleView() (tea.Model, tea.Cmd) {
@ -2181,6 +2217,8 @@ func (m *Model) openHelpModal() {
" t cycle theme", " t cycle theme",
"", "",
"Dialogs and Transfers", "Dialogs and Transfers",
" F8 / x move selected entry to trash",
" F11 / d permanently delete selected entry",
" r rename selected entry", " r rename selected entry",
" Enter / y confirm action", " Enter / y confirm action",
" Esc / n cancel action", " Esc / n cancel action",
@ -3526,7 +3564,9 @@ func (p pendingOperation) cmd() tea.Cmd {
case opMove: case opMove:
return nil return nil
case opDelete: case opDelete:
return deletePathsCmd(p.sourcePaths) return trashPathsCmd(p.sourcePaths)
case opPermanentDelete:
return deletePathsPermanentCmd(p.sourcePaths)
default: default:
return nil return nil
} }
@ -3607,27 +3647,6 @@ func (m *Model) cleanupArchiveMounts() {
} }
} }
func deletePlanCmd(sourcePaths []string) tea.Cmd {
return func() tea.Msg {
stats := vfs.TransferStats{}
var err error
for _, sourcePath := range sourcePaths {
part, statErr := vfs.CopyStats(sourcePath)
if statErr != nil {
err = statErr
break
}
stats.FilesTotal += part.FilesTotal
stats.BytesTotal += part.BytesTotal
}
return deletePlanMsg{
sourcePaths: append([]string(nil), sourcePaths...),
stats: stats,
err: err,
}
}
}
func archivePlanCmd(sourcePaths []string, targetDir string) tea.Cmd { func archivePlanCmd(sourcePaths []string, targetDir string) tea.Cmd {
return func() tea.Msg { return func() tea.Msg {
stats := vfs.TransferStats{} stats := vfs.TransferStats{}
@ -3822,10 +3841,10 @@ func moveCmd(sourcePath, targetDir string, overwrite bool) tea.Cmd {
} }
} }
func deletePathsCmd(paths []string) tea.Cmd { func trashPathsCmd(paths []string) tea.Cmd {
return func() tea.Msg { return func() tea.Msg {
for _, path := range paths { for _, path := range paths {
if err := vfs.DeletePath(path); err != nil { if err := vfs.MoveToTrash(path); err != nil {
return opMsg{kind: opDelete, sourcePath: path, err: err} return opMsg{kind: opDelete, sourcePath: path, err: err}
} }
} }
@ -3833,6 +3852,61 @@ func deletePathsCmd(paths []string) tea.Cmd {
} }
} }
func deletePathsPermanentCmd(paths []string) tea.Cmd {
return func() tea.Msg {
for _, path := range paths {
if err := vfs.DeletePath(path); err != nil {
return opMsg{kind: opPermanentDelete, sourcePath: path, err: err}
}
}
return opMsg{kind: opPermanentDelete}
}
}
func trashPlanCmd(sourcePaths []string) tea.Cmd {
return func() tea.Msg {
stats := vfs.TransferStats{}
var err error
for _, sourcePath := range sourcePaths {
part, statErr := vfs.CopyStats(sourcePath)
if statErr != nil {
err = statErr
break
}
stats.FilesTotal += part.FilesTotal
stats.BytesTotal += part.BytesTotal
}
return deletePlanMsg{
kind: opDelete,
sourcePaths: append([]string(nil), sourcePaths...),
stats: stats,
err: err,
}
}
}
func deletePlanPermanentCmd(sourcePaths []string) tea.Cmd {
return func() tea.Msg {
stats := vfs.TransferStats{}
var err error
for _, sourcePath := range sourcePaths {
part, statErr := vfs.CopyStats(sourcePath)
if statErr != nil {
err = statErr
break
}
stats.FilesTotal += part.FilesTotal
stats.BytesTotal += part.BytesTotal
}
return deletePlanMsg{
kind: opPermanentDelete,
sourcePaths: append([]string(nil), sourcePaths...),
stats: stats,
err: err,
}
}
}
func mkdirCmd(parent, name string) tea.Cmd { func mkdirCmd(parent, name string) tea.Cmd {
return func() tea.Msg { return func() tea.Msg {
targetPath, err := vfs.MakeDir(parent, name) targetPath, err := vfs.MakeDir(parent, name)
@ -3932,7 +4006,9 @@ func operationDoneLabel(kind fileOpKind) string {
case opCopy: case opCopy:
return "Copied" return "Copied"
case opDelete: case opDelete:
return "Deleted" return "Moved to trash"
case opPermanentDelete:
return "Permanently deleted"
case opArchive: case opArchive:
return "Archived" return "Archived"
default: default:
@ -3947,7 +4023,9 @@ func operationVerb(kind fileOpKind) string {
case opMove: case opMove:
return "move" return "move"
case opDelete: case opDelete:
return "delete" return "trash"
case opPermanentDelete:
return "permanent delete"
case opArchive: case opArchive:
return "archive" return "archive"
default: default: