Add transfer progress workflow for move and polish labels
This commit is contained in:
parent
a196a16c6f
commit
5a5923099b
2 changed files with 94 additions and 27 deletions
|
|
@ -141,6 +141,10 @@ func CopyPathWithProgress(srcPath string, dstDir string, overwrite bool, stats T
|
||||||
}
|
}
|
||||||
|
|
||||||
func MovePath(srcPath string, dstDir string, overwrite bool) (string, error) {
|
func MovePath(srcPath string, dstDir string, overwrite bool) (string, error) {
|
||||||
|
return MovePathWithProgress(srcPath, dstDir, overwrite, TransferStats{}, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func MovePathWithProgress(srcPath string, dstDir string, overwrite bool, stats TransferStats, progress func(CopyProgress)) (string, error) {
|
||||||
targetPath := filepath.Join(dstDir, filepath.Base(srcPath))
|
targetPath := filepath.Join(dstDir, filepath.Base(srcPath))
|
||||||
if same, err := samePath(srcPath, targetPath); err != nil {
|
if same, err := samePath(srcPath, targetPath); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
|
|
@ -159,13 +163,31 @@ func MovePath(srcPath string, dstDir string, overwrite bool) (string, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if progress == nil {
|
||||||
|
progress = func(CopyProgress) {}
|
||||||
|
}
|
||||||
|
if stats.FilesTotal == 0 && stats.BytesTotal == 0 {
|
||||||
|
resolved, err := CopyStats(srcPath)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
stats = resolved
|
||||||
|
}
|
||||||
|
|
||||||
if err := os.Rename(srcPath, targetPath); err == nil {
|
if err := os.Rename(srcPath, targetPath); err == nil {
|
||||||
|
progress(CopyProgress{
|
||||||
|
FilesDone: stats.FilesTotal,
|
||||||
|
FilesTotal: stats.FilesTotal,
|
||||||
|
BytesDone: stats.BytesTotal,
|
||||||
|
BytesTotal: stats.BytesTotal,
|
||||||
|
CurrentPath: srcPath,
|
||||||
|
})
|
||||||
return targetPath, nil
|
return targetPath, nil
|
||||||
} else if !errors.Is(err, syscall.EXDEV) {
|
} else if !errors.Is(err, syscall.EXDEV) {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
targetPath, err := CopyPath(srcPath, dstDir, overwrite)
|
targetPath, err := CopyPathWithProgress(srcPath, dstDir, overwrite, stats, progress)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -77,6 +77,7 @@ type opMsg struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type copyPlanMsg struct {
|
type copyPlanMsg struct {
|
||||||
|
kind fileOpKind
|
||||||
sourcePath string
|
sourcePath string
|
||||||
targetDir string
|
targetDir string
|
||||||
targetPath string
|
targetPath string
|
||||||
|
|
@ -92,6 +93,7 @@ type copyProgressMsg struct {
|
||||||
|
|
||||||
type copyDoneMsg struct {
|
type copyDoneMsg struct {
|
||||||
jobID int
|
jobID int
|
||||||
|
kind fileOpKind
|
||||||
sourcePath string
|
sourcePath string
|
||||||
targetPath string
|
targetPath string
|
||||||
err error
|
err error
|
||||||
|
|
@ -101,6 +103,7 @@ type dismissNoticeMsg struct{}
|
||||||
|
|
||||||
type copyJobState struct {
|
type copyJobState struct {
|
||||||
id int
|
id int
|
||||||
|
kind fileOpKind
|
||||||
sourcePath string
|
sourcePath string
|
||||||
targetDir string
|
targetDir string
|
||||||
targetPath string
|
targetPath string
|
||||||
|
|
@ -263,20 +266,21 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
title := "Copy selected entry?"
|
verb := operationVerb(msg.kind)
|
||||||
|
title := fmt.Sprintf("%s selected entry?", strings.Title(verb))
|
||||||
body := strings.Join([]string{
|
body := strings.Join([]string{
|
||||||
fmt.Sprintf("From: %s", msg.sourcePath),
|
fmt.Sprintf("From: %s", msg.sourcePath),
|
||||||
fmt.Sprintf("To: %s", msg.targetPath),
|
fmt.Sprintf("To: %s", msg.targetPath),
|
||||||
"",
|
"",
|
||||||
fmt.Sprintf("Files: %d", msg.stats.FilesTotal),
|
fmt.Sprintf("Files: %d", msg.stats.FilesTotal),
|
||||||
fmt.Sprintf("Data: %s", formatSize(msg.stats.BytesTotal, true)),
|
fmt.Sprintf("Size: %s", formatSize(msg.stats.BytesTotal, true)),
|
||||||
}, "\n")
|
}, "\n")
|
||||||
if msg.overwrite {
|
if msg.overwrite {
|
||||||
body += "\n\nTarget exists and will be overwritten."
|
body += "\n\nTarget exists and will be overwritten."
|
||||||
}
|
}
|
||||||
note := "Enter/y to start copy, Esc/n to cancel"
|
note := fmt.Sprintf("Enter/y to start %s, Esc/n to cancel", verb)
|
||||||
m.openConfirmModal(title, body, note, pendingOperation{
|
m.openConfirmModal(title, body, note, pendingOperation{
|
||||||
kind: opCopy,
|
kind: msg.kind,
|
||||||
sourcePath: msg.sourcePath,
|
sourcePath: msg.sourcePath,
|
||||||
targetDir: msg.targetDir,
|
targetDir: msg.targetDir,
|
||||||
overwrite: msg.overwrite,
|
overwrite: msg.overwrite,
|
||||||
|
|
@ -290,7 +294,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
}
|
}
|
||||||
m.copyJob.progress = msg.progress
|
m.copyJob.progress = msg.progress
|
||||||
if m.copyJob.background {
|
if m.copyJob.background {
|
||||||
m.status = formatCopyStatus(msg.progress)
|
m.status = formatCopyStatus(m.copyJob.kind, msg.progress)
|
||||||
}
|
}
|
||||||
return m, waitCopyProgressCmd(m.copyProgress)
|
return m, waitCopyProgressCmd(m.copyProgress)
|
||||||
|
|
||||||
|
|
@ -301,7 +305,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
|
||||||
m.busy = false
|
m.busy = false
|
||||||
if msg.err != nil {
|
if msg.err != nil {
|
||||||
m.status = fmt.Sprintf("Copy failed: %v", msg.err)
|
m.status = fmt.Sprintf("%s failed: %v", strings.Title(operationVerb(msg.kind)), msg.err)
|
||||||
m.copyJob = nil
|
m.copyJob = nil
|
||||||
if m.modal.kind == modalCopyProgress {
|
if m.modal.kind == modalCopyProgress {
|
||||||
m.modal = modalState{}
|
m.modal = modalState{}
|
||||||
|
|
@ -309,11 +313,12 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
m.status = fmt.Sprintf("Copied to %s", msg.targetPath)
|
m.status = fmt.Sprintf("%s to %s", operationDoneLabel(msg.kind), msg.targetPath)
|
||||||
activeSelection := selectedName(m.activePane())
|
activeSelection := selectedName(m.activePane())
|
||||||
_ = m.reloadPane(PaneLeft, activeSelection)
|
_ = m.reloadPane(PaneLeft, activeSelection)
|
||||||
_ = m.reloadPane(PaneRight, activeSelection)
|
_ = m.reloadPane(PaneRight, activeSelection)
|
||||||
background := m.copyJob.background
|
background := m.copyJob.background
|
||||||
|
kind := m.copyJob.kind
|
||||||
m.copyJob = nil
|
m.copyJob = nil
|
||||||
|
|
||||||
cmd := m.loadPreviewCmd()
|
cmd := m.loadPreviewCmd()
|
||||||
|
|
@ -321,10 +326,14 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
m.modal = modalState{}
|
m.modal = modalState{}
|
||||||
}
|
}
|
||||||
if background {
|
if background {
|
||||||
|
doneWord := "copied"
|
||||||
|
if kind == opMove {
|
||||||
|
doneWord = "moved"
|
||||||
|
}
|
||||||
m.modal = modalState{
|
m.modal = modalState{
|
||||||
kind: modalNotice,
|
kind: modalNotice,
|
||||||
title: "Copy complete",
|
title: strings.Title(operationVerb(kind)) + " complete",
|
||||||
body: filepath.Base(msg.sourcePath) + " copied successfully.",
|
body: filepath.Base(msg.sourcePath) + " " + doneWord + " successfully.",
|
||||||
}
|
}
|
||||||
cmd = tea.Batch(cmd, dismissNoticeCmd(time.Second))
|
cmd = tea.Batch(cmd, dismissNoticeCmd(time.Second))
|
||||||
}
|
}
|
||||||
|
|
@ -508,13 +517,13 @@ func (m Model) handleModalKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||||
}
|
}
|
||||||
pending := *m.modal.pending
|
pending := *m.modal.pending
|
||||||
m.modal = modalState{}
|
m.modal = modalState{}
|
||||||
if pending.kind == opCopy {
|
if pending.kind == opCopy || pending.kind == opMove {
|
||||||
if m.copyJob != nil {
|
if m.copyJob != nil {
|
||||||
m.status = "Copy is already running"
|
m.status = "Transfer is already running"
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
m.busy = true
|
m.busy = true
|
||||||
return m, m.startCopyJob(pending.sourcePath, pending.targetDir, pending.overwrite, pending.stats)
|
return m, m.startCopyJob(pending.kind, pending.sourcePath, pending.targetDir, pending.overwrite, pending.stats)
|
||||||
}
|
}
|
||||||
m.busy = true
|
m.busy = true
|
||||||
return m, pending.cmd()
|
return m, pending.cmd()
|
||||||
|
|
@ -528,7 +537,7 @@ func (m Model) handleModalKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||||
}
|
}
|
||||||
m.copyJob.background = true
|
m.copyJob.background = true
|
||||||
m.modal = modalState{}
|
m.modal = modalState{}
|
||||||
m.status = "Copy continues in background"
|
m.status = "Transfer continues in background"
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
|
|
@ -704,9 +713,9 @@ func (m *Model) handleTransfer(kind fileOpKind) (tea.Model, tea.Cmd) {
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if kind == opCopy {
|
if kind == opCopy || kind == opMove {
|
||||||
if m.copyJob != nil {
|
if m.copyJob != nil {
|
||||||
m.status = "Copy is already running"
|
m.status = "Transfer is already running"
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -715,8 +724,8 @@ func (m *Model) handleTransfer(kind fileOpKind) (tea.Model, tea.Cmd) {
|
||||||
overwrite = true
|
overwrite = true
|
||||||
}
|
}
|
||||||
m.busy = true
|
m.busy = true
|
||||||
m.status = fmt.Sprintf("Calculating copy size for %s", selected.DisplayName())
|
m.status = fmt.Sprintf("Calculating %s size for %s", operationVerb(kind), selected.DisplayName())
|
||||||
return m, copyPlanCmd(selected.Path, targetDir, overwrite)
|
return m, copyPlanCmd(kind, selected.Path, targetDir, overwrite)
|
||||||
}
|
}
|
||||||
|
|
||||||
if exists && m.cfg.Behavior.ConfirmOverwrite {
|
if exists && m.cfg.Behavior.ConfirmOverwrite {
|
||||||
|
|
@ -1340,13 +1349,13 @@ func renderCopyProgressModal(job copyJobState, palette theme.Palette, width int)
|
||||||
}
|
}
|
||||||
|
|
||||||
lines := []string{
|
lines := []string{
|
||||||
titleStyle.Render("Copying"),
|
titleStyle.Render(progressTitle(job.kind)),
|
||||||
lineStyle.Render(fmt.Sprintf("From: %s", job.sourcePath)),
|
lineStyle.Render(fmt.Sprintf("From: %s", job.sourcePath)),
|
||||||
lineStyle.Render(fmt.Sprintf("To: %s", job.targetPath)),
|
lineStyle.Render(fmt.Sprintf("To: %s", job.targetPath)),
|
||||||
spacer,
|
spacer,
|
||||||
lineStyle.Render(renderProgressBar(ratio, max(width-8, 10), palette)),
|
lineStyle.Render(renderProgressBar(ratio, max(width-8, 10), palette)),
|
||||||
lineStyle.Render(fmt.Sprintf("Files: %d / %d", progress.FilesDone, progress.FilesTotal)),
|
lineStyle.Render(fmt.Sprintf("Files: %d / %d", progress.FilesDone, progress.FilesTotal)),
|
||||||
lineStyle.Render(fmt.Sprintf("Data: %s / %s", formatSize(progress.BytesDone, true), formatSize(progress.BytesTotal, true))),
|
lineStyle.Render(fmt.Sprintf("Size: %s / %s", formatSize(progress.BytesDone, true), formatSize(progress.BytesTotal, true))),
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.TrimSpace(progress.CurrentPath) != "" {
|
if strings.TrimSpace(progress.CurrentPath) != "" {
|
||||||
|
|
@ -1490,10 +1499,11 @@ func dirSizeCmd(path string) tea.Cmd {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func copyPlanCmd(sourcePath, targetDir string, overwrite bool) tea.Cmd {
|
func copyPlanCmd(kind fileOpKind, sourcePath, targetDir string, overwrite bool) tea.Cmd {
|
||||||
return func() tea.Msg {
|
return func() tea.Msg {
|
||||||
stats, err := vfs.CopyStats(sourcePath)
|
stats, err := vfs.CopyStats(sourcePath)
|
||||||
return copyPlanMsg{
|
return copyPlanMsg{
|
||||||
|
kind: kind,
|
||||||
sourcePath: sourcePath,
|
sourcePath: sourcePath,
|
||||||
targetDir: targetDir,
|
targetDir: targetDir,
|
||||||
targetPath: filepath.Join(targetDir, filepath.Base(sourcePath)),
|
targetPath: filepath.Join(targetDir, filepath.Base(sourcePath)),
|
||||||
|
|
@ -1516,12 +1526,13 @@ func dismissNoticeCmd(delay time.Duration) tea.Cmd {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) startCopyJob(sourcePath, targetDir string, overwrite bool, stats vfs.TransferStats) tea.Cmd {
|
func (m *Model) startCopyJob(kind fileOpKind, sourcePath, targetDir string, overwrite bool, stats vfs.TransferStats) tea.Cmd {
|
||||||
m.nextCopyJob++
|
m.nextCopyJob++
|
||||||
jobID := m.nextCopyJob
|
jobID := m.nextCopyJob
|
||||||
targetPath := filepath.Join(targetDir, filepath.Base(sourcePath))
|
targetPath := filepath.Join(targetDir, filepath.Base(sourcePath))
|
||||||
m.copyJob = ©JobState{
|
m.copyJob = ©JobState{
|
||||||
id: jobID,
|
id: jobID,
|
||||||
|
kind: kind,
|
||||||
sourcePath: sourcePath,
|
sourcePath: sourcePath,
|
||||||
targetDir: targetDir,
|
targetDir: targetDir,
|
||||||
targetPath: targetPath,
|
targetPath: targetPath,
|
||||||
|
|
@ -1535,16 +1546,27 @@ func (m *Model) startCopyJob(sourcePath, targetDir string, overwrite bool, stats
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
m.modal = modalState{kind: modalCopyProgress}
|
m.modal = modalState{kind: modalCopyProgress}
|
||||||
m.status = "Copy started"
|
m.status = strings.Title(operationVerb(kind)) + " started"
|
||||||
|
|
||||||
return tea.Batch(
|
return tea.Batch(
|
||||||
func() tea.Msg {
|
func() tea.Msg {
|
||||||
go func() {
|
go func() {
|
||||||
target, err := vfs.CopyPathWithProgress(sourcePath, targetDir, overwrite, stats, func(progress vfs.CopyProgress) {
|
var (
|
||||||
|
target string
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
progressFn := func(progress vfs.CopyProgress) {
|
||||||
m.copyProgress <- copyProgressMsg{jobID: jobID, progress: progress}
|
m.copyProgress <- copyProgressMsg{jobID: jobID, progress: progress}
|
||||||
})
|
}
|
||||||
|
switch kind {
|
||||||
|
case opMove:
|
||||||
|
target, err = vfs.MovePathWithProgress(sourcePath, targetDir, overwrite, stats, progressFn)
|
||||||
|
default:
|
||||||
|
target, err = vfs.CopyPathWithProgress(sourcePath, targetDir, overwrite, stats, progressFn)
|
||||||
|
}
|
||||||
m.copyProgress <- copyDoneMsg{
|
m.copyProgress <- copyDoneMsg{
|
||||||
jobID: jobID,
|
jobID: jobID,
|
||||||
|
kind: kind,
|
||||||
sourcePath: sourcePath,
|
sourcePath: sourcePath,
|
||||||
targetPath: target,
|
targetPath: target,
|
||||||
err: err,
|
err: err,
|
||||||
|
|
@ -1606,9 +1628,10 @@ func formatSize(size int64, human bool) string {
|
||||||
return fmt.Sprintf("%d", size)
|
return fmt.Sprintf("%d", size)
|
||||||
}
|
}
|
||||||
|
|
||||||
func formatCopyStatus(progress vfs.CopyProgress) string {
|
func formatCopyStatus(kind fileOpKind, progress vfs.CopyProgress) string {
|
||||||
return fmt.Sprintf(
|
return fmt.Sprintf(
|
||||||
"Copy in background: %d/%d files, %s/%s",
|
"%s in background: %d/%d files, %s/%s",
|
||||||
|
strings.Title(operationVerb(kind)),
|
||||||
progress.FilesDone,
|
progress.FilesDone,
|
||||||
progress.FilesTotal,
|
progress.FilesTotal,
|
||||||
formatSize(progress.BytesDone, true),
|
formatSize(progress.BytesDone, true),
|
||||||
|
|
@ -1616,6 +1639,28 @@ func formatCopyStatus(progress vfs.CopyProgress) string {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func progressTitle(kind fileOpKind) string {
|
||||||
|
switch kind {
|
||||||
|
case opMove:
|
||||||
|
return "Moving"
|
||||||
|
default:
|
||||||
|
return "Copying"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func operationDoneLabel(kind fileOpKind) string {
|
||||||
|
switch kind {
|
||||||
|
case opMove:
|
||||||
|
return "Moved"
|
||||||
|
case opCopy:
|
||||||
|
return "Copied"
|
||||||
|
case opDelete:
|
||||||
|
return "Deleted"
|
||||||
|
default:
|
||||||
|
return "Done"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func operationVerb(kind fileOpKind) string {
|
func operationVerb(kind fileOpKind) string {
|
||||||
switch kind {
|
switch kind {
|
||||||
case opCopy:
|
case opCopy:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue