Skip to content

Commit e1f85ef

Browse files
authored
feat: Remove channel for notification models, refactor the notify and warn modal into a common modal, and other improvements (#979)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Introduced a new notification modal system for confirmations and warnings, including rename, delete, and quit actions. * Added clearer modal button labels with hotkey hints for better usability. * **Refactor** * Replaced old warning modals and channel-based messaging with a unified notification modal system. * Updated key handling and quit confirmation flows to integrate with the new notification modals. * Standardized modal key handling and command returns for improved user input processing. * Centralized UI notifications and warnings using Bubble Tea commands with request IDs. * Simplified paste operation validation with user-friendly error notifications. * **Bug Fixes** * Improved validation and error feedback for file operations like paste, rename, and delete using notification modals. * **Tests** * Added tests for file rename operations, including conflict warnings and confirmation handling. * Adjusted tests to reflect updated quit confirmation states and notification modal behavior. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
2 parents 5ae54a3 + 16b2048 commit e1f85ef

15 files changed

+372
-313
lines changed

src/internal/common/predefined_variable.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ const WheelRunTime = 5
1111
const DefaultCommandTimeout = 5000 * time.Millisecond
1212
const DateModifiedOption = "Date Modified"
1313

14+
const SameRenameWarnTitle = "There is already a file or directory with that name"
15+
const SameRenameWarnContent = "This operation will override the existing file"
16+
1417
var (
1518
MinimumHeight = 24
1619
MinimumWidth = 60
@@ -41,7 +44,11 @@ var (
4144
FilePreviewEmptyText string
4245
FilePreviewError string
4346

44-
LipglossError string
47+
ModalConfirmInputText string
48+
ModalCancelInputText string
49+
ModalOkayInputText string
50+
ModalInputSpacingText string
51+
LipglossError string
4552
)
4653

4754
var (
@@ -83,4 +90,10 @@ func LoadPrerenderedVariables() {
8390
FilePreviewDirectoryUnreadableText = "\n--- " + icon.Error + icon.Space + "Cannot read directory" + icon.Space + "---"
8491
FilePreviewError = "\n--- " + icon.Error + icon.Space + "Error" + icon.Space + "---"
8592
FilePreviewEmptyText = "\n--- Empty ---"
93+
94+
ModalOkayInputText = MainStyle.AlignHorizontal(lipgloss.Center).AlignVertical(lipgloss.Center).Render(
95+
ModalConfirm.Render(" (" + Hotkeys.Confirm[0] + ") Okay "))
96+
ModalConfirmInputText = ModalConfirm.Render(" (" + Hotkeys.Confirm[0] + ") Confirm ")
97+
ModalCancelInputText = ModalCancel.Render(" (" + Hotkeys.Quit[0] + ") Cancel ")
98+
ModalInputSpacingText = lipgloss.NewStyle().Background(ModalBGColor).Render(" ")
8699
}

src/internal/function.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,3 +361,10 @@ func processCmdToTeaCmd(cmd processbar.Cmd) tea.Cmd {
361361
}
362362
}
363363
}
364+
365+
func getCopyOrCutOperationName(cut bool) string {
366+
if cut {
367+
return "cut"
368+
}
369+
return "copy"
370+
}

src/internal/handle_file_operation_test.go

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -127,14 +127,7 @@ func TestCompressSelectedFiles(t *testing.T) {
127127
// on its own
128128
p.SendDirectly(nil)
129129

130-
cursorIndexForZip := -1
131-
for i, elem := range m.getFocusedFilePanel().element {
132-
if elem.location == zipFile {
133-
cursorIndexForZip = i
134-
break
135-
}
136-
}
137-
130+
cursorIndexForZip := findItemIndexInPanelByLocation(m.getFocusedFilePanel(), zipFile)
138131
require.NotEqual(t, -1, cursorIndexForZip)
139132

140133
m.getFocusedFilePanel().cursor = cursorIndexForZip

src/internal/handle_file_operations.go

Lines changed: 76 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,14 @@ import (
1212
"time"
1313

1414
variable "github.com/yorukot/superfile/src/config"
15+
"github.com/yorukot/superfile/src/internal/ui/notify"
1516
"github.com/yorukot/superfile/src/internal/ui/processbar"
1617
"github.com/yorukot/superfile/src/internal/utils"
1718

1819
"github.com/yorukot/superfile/src/internal/common"
1920

2021
"github.com/atotto/clipboard"
2122
tea "github.com/charmbracelet/bubbletea"
22-
"github.com/lithammer/shortuuid"
2323
"github.com/yorukot/superfile/src/config/icon"
2424
)
2525

@@ -57,20 +57,18 @@ func (m *model) IsRenamingConflicting() bool {
5757
}
5858

5959
// TODO: Remove channel messaging and use tea.Cmd
60-
func (m *model) warnModalForRenaming() {
61-
id := shortuuid.New()
62-
message := channelMessage{
63-
messageID: id,
64-
messageType: sendWarnModal,
65-
}
66-
67-
message.warnModal = warnModal{
68-
open: true,
69-
title: "There is already a file or directory with that name",
70-
content: "This operation will override the existing file",
71-
warnType: confirmRenameItem,
72-
}
73-
channel <- message
60+
func (m *model) warnModalForRenaming() tea.Cmd {
61+
reqID := m.ioReqCnt
62+
m.ioReqCnt++
63+
slog.Debug("Submitting rename notify model request", "reqID", reqID)
64+
res := func() tea.Msg {
65+
notifyModel := notify.New(true,
66+
common.SameRenameWarnTitle,
67+
common.SameRenameWarnContent,
68+
notify.RenameAction)
69+
return NewNotifyModalMsg(notifyModel, reqID)
70+
}
71+
return res
7472
}
7573

7674
// Rename file where the cusror is located
@@ -156,37 +154,26 @@ func deleteOperation(processBarModel *processbar.Model, items []string, useTrash
156154
return p.State
157155
}
158156

159-
func (m *model) deleteItemWarn() {
160-
panel := &m.fileModel.filePanels[m.filePanelFocusIndex]
161-
157+
func (m *model) getDeleteTriggerCmd() tea.Cmd {
158+
panel := m.getFocusedFilePanel()
162159
if (panel.panelMode == selectMode && len(panel.selected) == 0) ||
163160
(panel.panelMode == browserMode && len(panel.element) == 0) {
164-
return
161+
return nil
165162
}
166163

167-
id := shortuuid.New()
168-
message := channelMessage{
169-
messageID: id,
170-
messageType: sendWarnModal,
171-
}
164+
reqID := m.ioReqCnt
165+
m.ioReqCnt++
172166

173-
if !hasTrash || isExternalDiskPath(panel.location) {
174-
message.warnModal = warnModal{
175-
open: true,
176-
title: "Are you sure you want to completely delete",
177-
content: "This operation cannot be undone and your data will be completely lost.",
178-
warnType: confirmDeleteItem,
167+
return func() tea.Msg {
168+
title := "Are you sure you want to move this to trash can"
169+
content := "This operation will move file or directory to trash can."
170+
171+
if !hasTrash || isExternalDiskPath(panel.location) {
172+
title = "Are you sure you want to completely delete"
173+
content = "This operation cannot be undone and your data will be completely lost."
179174
}
180-
channel <- message
181-
return
175+
return NewNotifyModalMsg(notify.New(true, title, content, notify.DeleteAction), reqID)
182176
}
183-
message.warnModal = warnModal{
184-
open: true,
185-
title: "Are you sure you want to move this to trash can",
186-
content: "This operation will move file or directory to trash can.",
187-
warnType: confirmDeleteItem,
188-
}
189-
channel <- message
190177
}
191178

192179
// Copy directory or file's path to superfile's clipboard
@@ -229,86 +216,49 @@ func (m *model) getPasteItemCmd() tea.Cmd {
229216

230217
slog.Debug("Submitting pasteItems request", "id", reqID, "items cnt", len(copyItems), "dest", panelLocation)
231218
return func() tea.Msg {
219+
err := validatePasteOperation(panelLocation, copyItems, cut)
220+
if err != nil {
221+
return NewNotifyModalMsg(notify.New(true, "Invalid paste location", err.Error(), notify.NoAction),
222+
reqID)
223+
}
232224
state := executePasteOperation(&m.processBarModel, panelLocation, copyItems, cut)
233225
return NewPasteOperationMsg(state, reqID)
234226
}
235227
}
236228

237-
// Paste all clipboard items
238-
func executePasteOperation(processBarModel *processbar.Model,
239-
panelLocation string, copyItems []string, cut bool) processbar.ProcessState {
240-
if len(copyItems) == 0 {
241-
return processbar.Cancelled
242-
}
243-
244-
id := shortuuid.New()
245-
229+
func validatePasteOperation(panelLocation string, copyItems []string, cut bool) error {
246230
// Check if trying to paste into source or subdirectory for both cut and copy operations
247231
for _, srcPath := range copyItems {
248232
// Check if trying to cut and paste into the same directory - this would be a no-op
249233
// and could potentially cause issues, so we prevent it
250234
if filepath.Dir(srcPath) == panelLocation && cut {
251-
slog.Error("Cannot paste into parent directory of source", "src", srcPath, "dst", panelLocation)
252-
message := channelMessage{
253-
messageID: id,
254-
messageType: sendNotifyModal,
255-
notifyModal: notifyModal{
256-
open: true,
257-
title: "Invalid paste location",
258-
content: "Cannot paste into parent directory of source",
259-
},
260-
}
261-
channel <- message
262-
return processbar.Cancelled
235+
return fmt.Errorf("cannot paste into parent directory of source, srcPath : %v, panelLocation : %v",
236+
srcPath, panelLocation)
263237
}
264-
265-
slog.Debug("model.pasteItem", "srcPath", srcPath, "panel location", panelLocation)
266-
267238
if cut && srcPath == panelLocation {
268-
slog.Error("Cannot paste a directory into itself", "operation", "cut", "src", srcPath, "dst", panelLocation)
269-
message := channelMessage{
270-
messageID: id,
271-
messageType: sendNotifyModal,
272-
notifyModal: notifyModal{
273-
open: true,
274-
title: "Invalid paste location",
275-
content: "Cannot paste a directory into itself",
276-
},
277-
}
278-
channel <- message
279-
return processbar.Cancelled
239+
return errors.New("cannot paste a directory into itself")
280240
}
281241

282242
if isAncestor(srcPath, panelLocation) {
283-
operation := "copy"
284-
if cut {
285-
operation = "cut"
286-
}
287-
288-
slog.Error("Cannot paste a directory into itself or its subdirectory",
289-
"operation", operation, "src", srcPath, "dst", panelLocation)
290-
message := channelMessage{
291-
messageID: id,
292-
messageType: sendNotifyModal,
293-
notifyModal: notifyModal{
294-
open: true,
295-
title: "Invalid paste location",
296-
content: fmt.Sprintf("Cannot %s and paste a directory into itself or its subdirectory", operation),
297-
},
298-
}
299-
channel <- message
300-
return processbar.Cancelled
243+
return fmt.Errorf("cannot %s and paste a directory into itself or its subdirectory",
244+
getCopyOrCutOperationName(cut))
301245
}
302246
}
303247

304-
slog.Debug("model.pasteItem", "items", copyItems, "cut", cut, "panel location", panelLocation)
248+
return nil
249+
}
305250

306-
prefixIcon := icon.Copy + icon.Space
307-
if cut {
308-
prefixIcon = icon.Cut + icon.Space
309-
}
251+
// new func to check and return an error that will go in m.content
252+
// create a new error type
310253

311-
p, err := processBarModel.SendAddProcessMsg(prefixIcon+filepath.Base(copyItems[0]), len(copyItems), true)
254+
// Paste all clipboard items
255+
func executePasteOperation(processBarModel *processbar.Model,
256+
panelLocation string, copyItems []string, cut bool) processbar.ProcessState {
257+
slog.Debug("executePasteOperation", "items", copyItems, "cut", cut, "panel location", panelLocation)
258+
259+
p, err := processBarModel.SendAddProcessMsg(
260+
icon.GetCopyOrCutIcon(cut)+icon.Space+filepath.Base(copyItems[0]),
261+
getTotalFilesCnt(copyItems), true)
312262
if err != nil {
313263
slog.Error("Cannot spawn a new process", "error", err)
314264
return processbar.Failed
@@ -330,25 +280,20 @@ func executePasteOperation(processBarModel *processbar.Model,
330280
}
331281
}
332282

333-
prefixIcon := icon.Copy
334-
if cut {
335-
prefixIcon = icon.Cut
336-
}
337-
p.Name = prefixIcon + icon.Space + filepath.Base(filePath)
283+
p.Name = icon.GetCopyOrCutIcon(cut) + icon.Space + filepath.Base(filePath)
338284
if err != nil {
339285
slog.Debug("model.pasteItem - paste failure", "error", err,
340286
"current item", filePath, "errMessage", errMessage)
341287
p.State = processbar.Failed
342288
slog.Error(errMessage, "error", err)
343289
break
344290
}
345-
346-
p.Done++
347291
processBarModel.TrySendingUpdateProcessMsg(p)
348292
}
349293

350294
if p.State != processbar.Failed {
351295
p.State = processbar.Successful
296+
p.Done = p.Total
352297
}
353298
p.DoneTime = time.Now()
354299
err = processBarModel.SendUpdateProcessMsg(p, true)
@@ -359,6 +304,31 @@ func executePasteOperation(processBarModel *processbar.Model,
359304
return p.State
360305
}
361306

307+
func getTotalFilesCnt(copyItems []string) int {
308+
totalFiles := 0
309+
for _, folderPath := range copyItems {
310+
// TODO : Fix this. This is inefficient
311+
// In case of a cut operations for a directory with a lot of files
312+
// we are unnecessarily walking the whole directory recursively
313+
// while os will just perform a rename
314+
// So instead of few operations this will cause the cut paste
315+
// to read the whole directory recursively
316+
// we should avoid doing this.
317+
// Although this allows us a more detailed progress tracking
318+
// this make the copy/cut more inefficient
319+
// instead, we could just track progress based on total items in
320+
// copyItems
321+
// efficiency should be prioritized over more detailed feedback.
322+
count, err := countFiles(folderPath)
323+
if err != nil {
324+
slog.Error("Error in countFiles", "error", err)
325+
continue
326+
}
327+
totalFiles += count
328+
}
329+
return totalFiles
330+
}
331+
362332
// Extract compressed file
363333
// TODO : err should be returned and properly handled by the caller
364334
func (m *model) getExtractFileCmd() tea.Cmd {

src/internal/handle_modal.go

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,6 @@ func (m *model) cancelTypingModal() {
1313
m.typingModal.open = false
1414
}
1515

16-
// Close warn modal
17-
func (m *model) cancelWarnModal() {
18-
m.warnModal.open = false
19-
}
20-
2116
// Confirm to create file or directory
2217
func (m *model) createItem() {
2318
if err := checkFileNameValidity(m.typingModal.textInput.Value()); err != nil {

0 commit comments

Comments
 (0)