Skip to content

Commit 20eac4d

Browse files
committed
when moving a file make it possible to change the name of the file in the target location
1 parent a435e47 commit 20eac4d

3 files changed

Lines changed: 80 additions & 0 deletions

File tree

internal/actions/move.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,58 @@ func Move(ctx context.Context, sources []string, destDir string, progressFn func
7272
return nil
7373
}
7474

75+
// MoveAs moves a single source to destPath (a full path, not a directory).
76+
// Used for single-item move where the user may have renamed the target.
77+
// Tries os.Rename first (fast, same device), falls back to copy+delete for
78+
// cross-device moves.
79+
func MoveAs(ctx context.Context, source, destPath string, progressFn func(Progress)) error {
80+
if err := ctx.Err(); err != nil {
81+
return ErrCancelled
82+
}
83+
84+
if absEq(source, destPath) {
85+
return fmt.Errorf("source and destination are the same: %s", source)
86+
}
87+
88+
totalFiles, totalBytes := countFilesAndBytes([]string{source})
89+
agg := Progress{
90+
Op: OpMove,
91+
TotalFiles: totalFiles,
92+
TotalBytes: totalBytes,
93+
}
94+
95+
// Try rename first (instant if same filesystem).
96+
if err := os.Rename(source, destPath); err == nil {
97+
agg.DoneFiles = totalFiles
98+
agg.DoneBytes = totalBytes
99+
agg.Current = filepath.Base(destPath)
100+
if progressFn != nil {
101+
progressFn(agg)
102+
}
103+
return nil
104+
}
105+
106+
// Cross-device: copy then delete. Forward copy progress as a move op so
107+
// the dialog keeps showing "Moving".
108+
forward := func(p Progress) {
109+
agg.Current = p.Current
110+
agg.FileTotalBytes = p.FileTotalBytes
111+
agg.FileDoneBytes = p.FileDoneBytes
112+
agg.DoneFiles = p.DoneFiles
113+
agg.DoneBytes = p.DoneBytes
114+
if progressFn != nil {
115+
progressFn(agg)
116+
}
117+
}
118+
if err := CopyAs(ctx, source, destPath, forward); err != nil {
119+
return fmt.Errorf("move (copy phase) %s: %w", source, err)
120+
}
121+
if err := os.RemoveAll(source); err != nil {
122+
return fmt.Errorf("move (delete phase) %s: %w", source, err)
123+
}
124+
return nil
125+
}
126+
75127
// Rename renames a single file or directory.
76128
func Rename(oldPath, newName string) error {
77129
dir := filepath.Dir(oldPath)

internal/app/app.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ const (
4343
tagCopy = "copy"
4444
tagCopyAs = "copyas"
4545
tagMove = "move"
46+
tagMoveAs = "moveas"
4647
tagDelete = "delete"
4748
tagMkdir = "mkdir"
4849
tagRename = "rename"
@@ -721,6 +722,13 @@ func (m Model) startMove() (tea.Model, tea.Cmd) {
721722
m.pendingSources = sources
722723
m.pendingDest = dest
723724

725+
if len(sources) == 1 {
726+
defaultPath := filepath.Join(dest, filepath.Base(sources[0]))
727+
d := dialog.NewInput("Move", "Move to:", defaultPath, tagMoveAs)
728+
m.dialog = &d
729+
return m, nil
730+
}
731+
724732
msg := fmt.Sprintf("Move %d item(s) to %s?", len(sources), dest)
725733
d := dialog.NewConfirm("Move", msg, tagMove)
726734
m.dialog = &d
@@ -890,6 +898,18 @@ func (m Model) handleDialogResult(result dialog.Result) (tea.Model, tea.Cmd) {
890898
waitForProgress(ch),
891899
)
892900
}
901+
case tagMoveAs:
902+
if result.Confirmed && strings.TrimSpace(result.Text) != "" && len(m.pendingSources) == 1 {
903+
target := expandHome(result.Text)
904+
if !filepath.IsAbs(target) {
905+
target = filepath.Join(m.pendingDest, target)
906+
}
907+
ctx, ch := m.startProgressOp("Moving")
908+
return m, tea.Batch(
909+
moveAsCmd(ctx, ch, m.pendingSources[0], target),
910+
waitForProgress(ch),
911+
)
912+
}
893913
case tagDelete:
894914
if result.Confirmed {
895915
ctx, ch := m.startProgressOp("Deleting")

internal/app/commands.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,14 @@ func moveCmd(ctx context.Context, ch chan actions.Progress, sources []string, de
8282
}
8383
}
8484

85+
func moveAsCmd(ctx context.Context, ch chan actions.Progress, source, destPath string) tea.Cmd {
86+
return func() tea.Msg {
87+
err := actions.MoveAs(ctx, source, destPath, sendProgress(ctx, ch))
88+
close(ch)
89+
return moveDoneMsg{err: err}
90+
}
91+
}
92+
8593
func deleteCmd(ctx context.Context, ch chan actions.Progress, paths []string) tea.Cmd {
8694
return func() tea.Msg {
8795
err := actions.Delete(ctx, paths, sendProgress(ctx, ch))

0 commit comments

Comments
 (0)