Skip to content

Commit 3e77de5

Browse files
committed
feat: added option to permanently delete files
1 parent f15e7a3 commit 3e77de5

File tree

12 files changed

+142
-26
lines changed

12 files changed

+142
-26
lines changed

src/internal/common/config_type.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -135,10 +135,11 @@ type HotkeysType struct {
135135
FilePanelItemCreate []string `toml:"file_panel_item_create" comment:"create file/directory and rename "`
136136
FilePanelItemRename []string `toml:"file_panel_item_rename"`
137137

138-
CopyItems []string `toml:"copy_items" comment:"file operate"`
139-
PasteItems []string `toml:"paste_items"`
140-
CutItems []string `toml:"cut_items"`
141-
DeleteItems []string `toml:"delete_items"`
138+
CopyItems []string `toml:"copy_items" comment:"file operate"`
139+
PasteItems []string `toml:"paste_items"`
140+
CutItems []string `toml:"cut_items"`
141+
DeleteItems []string `toml:"delete_items"`
142+
PermanentlyDeleteItems []string `toml:"permanently_delete_items"`
142143

143144
ExtractFile []string `toml:"extract_file" comment:"compress and extract"`
144145
CompressFile []string `toml:"compress_file"`

src/internal/default_config.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import (
1414

1515
// Generate and return model containing default configurations for interface
1616
// Maybe we can replace slice of strings with var args - Should we ?
17-
func defaultModelConfig(toggleDotFile bool, toggleFooter bool, firstUse bool, firstFilePanelDirs []string) *model {
17+
func defaultModelConfig(toggleDotFile, toggleFooter, firstUse, hasTrash bool, firstFilePanelDirs []string) *model {
1818
return &model{
1919
filePanelFocusIndex: 0,
2020
focusPanel: nonePanelFocus,
@@ -40,6 +40,7 @@ func defaultModelConfig(toggleDotFile bool, toggleFooter bool, firstUse bool, fi
4040
toggleDotFile: toggleDotFile,
4141
toggleFooter: toggleFooter,
4242
firstUse: firstUse,
43+
hasTrash: hasTrash,
4344
}
4445
}
4546

src/internal/handle_file_operations.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ func (m *model) panelItemRename() {
9292
panel.rename = common.GenerateRenameTextInput(m.fileModel.width-4, cursorPos, panel.element[panel.cursor].name)
9393
}
9494

95-
func (m *model) getDeleteCmd() tea.Cmd {
95+
func (m *model) getDeleteCmd(permDelete bool) tea.Cmd {
9696
panel := m.getFocusedFilePanel()
9797
if len(panel.element) == 0 {
9898
return nil
@@ -105,7 +105,7 @@ func (m *model) getDeleteCmd() tea.Cmd {
105105
items = []string{panel.getSelectedItem().location}
106106
}
107107

108-
useTrash := hasTrash && !isExternalDiskPath(panel.location)
108+
useTrash := m.hasTrash && !isExternalDiskPath(panel.location) && !permDelete
109109

110110
reqID := m.ioReqCnt
111111
m.ioReqCnt++
@@ -154,7 +154,7 @@ func deleteOperation(processBarModel *processbar.Model, items []string, useTrash
154154
return p.State
155155
}
156156

157-
func (m *model) getDeleteTriggerCmd() tea.Cmd {
157+
func (m *model) getDeleteTriggerCmd(deletePermanent bool) tea.Cmd {
158158
panel := m.getFocusedFilePanel()
159159
if (panel.panelMode == selectMode && len(panel.selected) == 0) ||
160160
(panel.panelMode == browserMode && len(panel.element) == 0) {
@@ -167,12 +167,14 @@ func (m *model) getDeleteTriggerCmd() tea.Cmd {
167167
return func() tea.Msg {
168168
title := "Are you sure you want to move this to trash can"
169169
content := "This operation will move file or directory to trash can."
170+
notifyModel := notify.New(true, title, content, notify.DeleteAction)
170171

171-
if !hasTrash || isExternalDiskPath(panel.location) {
172+
if !m.hasTrash || isExternalDiskPath(panel.location) || deletePermanent {
172173
title = "Are you sure you want to completely delete"
173174
content = "This operation cannot be undone and your data will be completely lost."
175+
notifyModel = notify.New(true, title, content, notify.PermanentDeleteAction)
174176
}
175-
return NewNotifyModalMsg(notify.New(true, title, content, notify.DeleteAction), reqID)
177+
return NewNotifyModalMsg(notifyModel, reqID)
176178
}
177179
}
178180

src/internal/key_function.go

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ func (m *model) mainKey(msg string) tea.Cmd { //nolint: gocyclo,cyclop,funlen //
128128

129129
func (m *model) normalAndBrowserModeKey(msg string) tea.Cmd {
130130
// if not focus on the filepanel return
131-
if m.fileModel.filePanels[m.filePanelFocusIndex].focusType != focus {
131+
if m.getFocusedFilePanel().focusType != focus {
132132
if m.focusPanel == sidebarFocus && slices.Contains(common.Hotkeys.Confirm, msg) {
133133
m.sidebarSelectDirectory()
134134
}
@@ -141,7 +141,7 @@ func (m *model) normalAndBrowserModeKey(msg string) tea.Cmd {
141141
return nil
142142
}
143143
// Check if in the select mode and focusOn filepanel
144-
if m.fileModel.filePanels[m.filePanelFocusIndex].panelMode == selectMode {
144+
if m.getFocusedFilePanel().panelMode == selectMode {
145145
switch {
146146
case slices.Contains(common.Hotkeys.Confirm, msg):
147147
m.fileModel.filePanels[m.filePanelFocusIndex].singleItemSelect()
@@ -150,7 +150,9 @@ func (m *model) normalAndBrowserModeKey(msg string) tea.Cmd {
150150
case slices.Contains(common.Hotkeys.FilePanelSelectModeItemsSelectDown, msg):
151151
m.fileModel.filePanels[m.filePanelFocusIndex].itemSelectDown(m.mainPanelHeight)
152152
case slices.Contains(common.Hotkeys.DeleteItems, msg):
153-
return m.getDeleteTriggerCmd()
153+
return m.getDeleteTriggerCmd(false)
154+
case slices.Contains(common.Hotkeys.PermanentlyDeleteItems, msg):
155+
return m.getDeleteTriggerCmd(true)
154156
case slices.Contains(common.Hotkeys.CopyItems, msg):
155157
m.copyMultipleItem(false)
156158
case slices.Contains(common.Hotkeys.CutItems, msg):
@@ -167,7 +169,9 @@ func (m *model) normalAndBrowserModeKey(msg string) tea.Cmd {
167169
case slices.Contains(common.Hotkeys.ParentDirectory, msg):
168170
m.parentDirectory()
169171
case slices.Contains(common.Hotkeys.DeleteItems, msg):
170-
return m.getDeleteTriggerCmd()
172+
return m.getDeleteTriggerCmd(false)
173+
case slices.Contains(common.Hotkeys.PermanentlyDeleteItems, msg):
174+
return m.getDeleteTriggerCmd(true)
171175
case slices.Contains(common.Hotkeys.CopyItems, msg):
172176
m.copySingleItem(false)
173177
case slices.Contains(common.Hotkeys.CutItems, msg):
@@ -217,7 +221,7 @@ func (m *model) handleNotifyModelCancel(action notify.ConfirmActionType) tea.Cmd
217221
m.cancelRename()
218222
case notify.QuitAction:
219223
m.modelQuitState = notQuitting
220-
case notify.DeleteAction, notify.NoAction:
224+
case notify.DeleteAction, notify.NoAction, notify.PermanentDeleteAction:
221225
// Do nothing
222226
default:
223227
slog.Error("Unknown type of action", "action", action)
@@ -228,7 +232,9 @@ func (m *model) handleNotifyModelCancel(action notify.ConfirmActionType) tea.Cmd
228232
func (m *model) handleNotifyModelConfirm(action notify.ConfirmActionType) tea.Cmd {
229233
switch action {
230234
case notify.DeleteAction:
231-
return m.getDeleteCmd()
235+
return m.getDeleteCmd(false)
236+
case notify.PermanentDeleteAction:
237+
return m.getDeleteCmd(true)
232238
case notify.RenameAction:
233239
m.confirmRename()
234240
case notify.QuitAction:

src/internal/model.go

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,23 +27,19 @@ import (
2727
)
2828

2929
// These represent model's state information, its not a global preperty
30-
var (
31-
LastTimeCursorMove = [2]int{int(time.Now().UnixMicro()), 0} //nolint: gochecknoglobals // TODO: Move to model struct
32-
hasTrash = true //nolint: gochecknoglobals // TODO: Move to model struct
33-
batCmd = "" //nolint: gochecknoglobals // TODO: Move to model struct
34-
et *exiftool.Exiftool //nolint: gochecknoglobals // TODO: Move to model struct
35-
)
30+
var LastTimeCursorMove = [2]int{int(time.Now().UnixMicro()), 0} //nolint: gochecknoglobals // TODO: Move to model struct
31+
var batCmd = "" //nolint: gochecknoglobals // TODO: Move to model struct
32+
var et *exiftool.Exiftool //nolint: gochecknoglobals // TODO: Move to model struct
3633

3734
// Initialize and return model with default configs
3835
// It returns only tea.Model because when it used in main, the return value
3936
// is passed to tea.NewProgram() which accepts tea.Model
4037
// Either way type 'model' is not exported, so there is not way main package can
4138
// be aware of it, and use it directly
42-
func InitialModel(firstFilePanelDirs []string, firstUseCheck, hasTrashCheck bool) tea.Model {
39+
func InitialModel(firstFilePanelDirs []string, firstUseCheck, hasTrash bool) tea.Model {
4340
toggleDotFile, toggleFooter := initialConfig(firstFilePanelDirs)
44-
hasTrash = hasTrashCheck
4541
batCmd = checkBatCmd()
46-
return defaultModelConfig(toggleDotFile, toggleFooter, firstUseCheck, firstFilePanelDirs)
42+
return defaultModelConfig(toggleDotFile, toggleFooter, firstUseCheck, hasTrash, firstFilePanelDirs)
4743
}
4844

4945
// Init function to be called by Bubble tea framework, sets windows title,

src/internal/model_file_operations_test.go

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@ import (
44
"fmt"
55
"os"
66
"path/filepath"
7+
"runtime"
78
"testing"
89
"time"
910

1011
tea "github.com/charmbracelet/bubbletea"
1112
"github.com/stretchr/testify/assert"
1213
"github.com/stretchr/testify/require"
14+
variable "github.com/yorukot/superfile/src/config"
1315
"github.com/yorukot/superfile/src/internal/common"
1416
"github.com/yorukot/superfile/src/internal/ui/notify"
1517
"github.com/yorukot/superfile/src/internal/utils"
@@ -187,3 +189,104 @@ func TestFileRename(t *testing.T) {
187189
actualTest(true)
188190
})
189191
}
192+
193+
func createDirectories(dirs ...string) error {
194+
for _, dir := range dirs {
195+
if err := os.MkdirAll(dir, 0755); err != nil {
196+
return fmt.Errorf("failed to create directory %s: %w", dir, err)
197+
}
198+
}
199+
return nil
200+
}
201+
202+
func initTrash() error {
203+
return createDirectories(
204+
variable.CustomTrashDirectory,
205+
variable.CustomTrashDirectoryFiles,
206+
variable.CustomTrashDirectoryInfo,
207+
)
208+
}
209+
210+
func getTrashPath(src string) string {
211+
home, _ := os.UserHomeDir()
212+
src = filepath.Base(src)
213+
switch runtime.GOOS {
214+
case utils.OsDarwin:
215+
return filepath.Join(variable.DarwinTrashDirectory, src)
216+
default:
217+
return filepath.Join(home, ".local", "share", "Trash", "files", src)
218+
}
219+
}
220+
221+
func TestFileDelete(t *testing.T) {
222+
curTestDir := t.TempDir()
223+
file1 := filepath.Join(curTestDir, "file1.txt")
224+
file2 := filepath.Join(curTestDir, "file2.txt")
225+
226+
setupFilesWithData(t, []byte("f1"), file1)
227+
setupFilesWithData(t, []byte("f2"), file2)
228+
229+
t.Run("move to trash", func(t *testing.T) {
230+
m := defaultTestModel(curTestDir)
231+
err := initTrash()
232+
if err == nil {
233+
m.hasTrash = true
234+
} else {
235+
fmt.Println("Unable to create trash directories.")
236+
}
237+
p := NewTestTeaProgWithEventLoop(t, m)
238+
idx := findItemIndexInPanelByLocation(m.getFocusedFilePanel(), file1)
239+
require.NotEqual(t, -1, idx, "%s should be found in panel", file1)
240+
m.getFocusedFilePanel().cursor = idx
241+
242+
p.SendKey(common.Hotkeys.DeleteItems[0])
243+
244+
assert.Eventually(t, func() bool {
245+
return m.notifyModel.IsOpen()
246+
}, time.Second, 10*time.Microsecond, "Notify model never opened")
247+
248+
p.Send(tea.KeyMsg{Type: tea.KeyRight})
249+
250+
assert.Eventually(t, func() bool {
251+
_, err1 := os.Stat(file1)
252+
trashFile := getTrashPath(file1)
253+
_, errTrash := os.Stat(trashFile)
254+
if runtime.GOOS == utils.OsWindows {
255+
return err1 != nil && os.IsNotExist(err1)
256+
}
257+
return err1 != nil && os.IsNotExist(err1) && errTrash == nil
258+
}, time.Second, 10*time.Millisecond, "File never moved to trash.")
259+
})
260+
261+
t.Run("move to trash", func(t *testing.T) {
262+
m := defaultTestModel(curTestDir)
263+
err := initTrash()
264+
if err == nil {
265+
m.hasTrash = true
266+
} else {
267+
fmt.Println("Unable to create trash directories.")
268+
}
269+
p := NewTestTeaProgWithEventLoop(t, m)
270+
idx := findItemIndexInPanelByLocation(m.getFocusedFilePanel(), file2)
271+
require.NotEqual(t, -1, idx, "%s should be found in panel", file2)
272+
m.getFocusedFilePanel().cursor = idx
273+
274+
p.SendKey(common.Hotkeys.PermanentlyDeleteItems[0])
275+
276+
assert.Eventually(t, func() bool {
277+
return m.notifyModel.IsOpen()
278+
}, time.Second, 10*time.Microsecond, "Notify model never opened")
279+
280+
p.Send(tea.KeyMsg{Type: tea.KeyRight})
281+
282+
assert.Eventually(t, func() bool {
283+
_, err1 := os.Stat(file2)
284+
trashFile := getTrashPath(file2)
285+
_, errTrash := os.Stat(trashFile)
286+
if runtime.GOOS == utils.OsWindows {
287+
return err1 != nil && os.IsNotExist(err1)
288+
}
289+
return err1 != nil && os.IsNotExist(err1) && errTrash != nil && os.IsNotExist(err1)
290+
}, time.Second, 10*time.Millisecond, "File never moved to trash.")
291+
})
292+
}

src/internal/test_utils.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ func setupFiles(t *testing.T, files ...string) {
4141
// -------------------- Model setup utils
4242

4343
func defaultTestModel(dirs ...string) *model {
44-
m := defaultModelConfig(false, false, false, dirs)
44+
m := defaultModelConfig(false, false, false, false, dirs)
4545
m.disableMetatdata = true
4646
_, _ = TeaUpdate(m, tea.WindowSizeMsg{Width: 2 * common.MinimumWidth, Height: 2 * common.MinimumHeight})
4747
return m

src/internal/type.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,9 @@ type model struct {
107107
footerHeight int
108108
fullWidth int
109109
fullHeight int
110+
111+
// wheather trash folder exists or not
112+
hasTrash bool
110113
}
111114

112115
// Modal

src/internal/ui/notify/type.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ const (
77
DeleteAction
88
QuitAction
99
NoAction
10+
PermanentDeleteAction
1011
)

src/superfile_config/hotkeys.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ copy_items = ['ctrl+c', '']
2828
cut_items = ['ctrl+x', '']
2929
paste_items = ['ctrl+v', 'ctrl+w', '']
3030
delete_items = ['ctrl+d', 'delete', '']
31+
permanently_delete_items = ['D', '']
3132
# compress and extract
3233
extract_file = ['ctrl+e', '']
3334
compress_file = ['ctrl+a', '']

0 commit comments

Comments
 (0)