Skip to content

Commit 263f144

Browse files
committed
feat: added option to permanently delete files
1 parent 4b888a1 commit 263f144

File tree

12 files changed

+139
-21
lines changed

12 files changed

+139
-21
lines changed

src/internal/common/config_type.go

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

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

141142
ExtractFile []string `toml:"extract_file" comment:"compress and extract"`
142143
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: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ import (
2828

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

@@ -37,11 +36,10 @@ var et *exiftool.Exiftool //nolint: gochec
3736
// is passed to tea.NewProgram() which accepts tea.Model
3837
// Either way type 'model' is not exported, so there is not way main package can
3938
// be aware of it, and use it directly
40-
func InitialModel(firstFilePanelDirs []string, firstUseCheck, hasTrashCheck bool) tea.Model {
39+
func InitialModel(firstFilePanelDirs []string, firstUseCheck, hasTrash bool) tea.Model {
4140
toggleDotFile, toggleFooter := initialConfig(firstFilePanelDirs)
42-
hasTrash = hasTrashCheck
4341
batCmd = checkBatCmd()
44-
return defaultModelConfig(toggleDotFile, toggleFooter, firstUseCheck, firstFilePanelDirs)
42+
return defaultModelConfig(toggleDotFile, toggleFooter, firstUseCheck, hasTrash, firstFilePanelDirs)
4543
}
4644

4745
// 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
@@ -104,6 +104,9 @@ type model struct {
104104
footerHeight int
105105
fullWidth int
106106
fullHeight int
107+
108+
// wheather trash folder exists or not
109+
hasTrash bool
107110
}
108111

109112
// 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)