Polish transfer progress dialog controls
This commit is contained in:
parent
ce84789edb
commit
95847ad231
3 changed files with 298 additions and 135 deletions
|
|
@ -1,6 +1,7 @@
|
||||||
package vfs
|
package vfs
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
|
@ -25,6 +26,7 @@ type CopyProgress struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type copyProgressState struct {
|
type copyProgressState struct {
|
||||||
|
ctx context.Context
|
||||||
filesDone int
|
filesDone int
|
||||||
bytesDone int64
|
bytesDone int64
|
||||||
stats TransferStats
|
stats TransferStats
|
||||||
|
|
@ -36,6 +38,87 @@ func CopyPath(srcPath string, dstDir string, overwrite bool) (string, error) {
|
||||||
return CopyPathWithProgress(srcPath, dstDir, overwrite, TransferStats{}, nil)
|
return CopyPathWithProgress(srcPath, dstDir, overwrite, TransferStats{}, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func CopyPathWithProgressContext(ctx context.Context, srcPath string, dstDir string, overwrite bool, stats TransferStats, progress func(CopyProgress)) (string, error) {
|
||||||
|
if ctx == nil {
|
||||||
|
ctx = context.Background()
|
||||||
|
}
|
||||||
|
|
||||||
|
srcInfo, err := os.Lstat(srcPath)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("stat %s: %w", srcPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
targetPath := filepath.Join(dstDir, filepath.Base(srcPath))
|
||||||
|
if same, err := samePath(srcPath, targetPath); err != nil {
|
||||||
|
return "", err
|
||||||
|
} else if same {
|
||||||
|
return "", fmt.Errorf("source and target are the same: %s", targetPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
if exists, err := PathExists(targetPath); err != nil {
|
||||||
|
return "", err
|
||||||
|
} else if exists {
|
||||||
|
if !overwrite {
|
||||||
|
return "", ErrOverwrite(targetPath)
|
||||||
|
}
|
||||||
|
if err := os.RemoveAll(targetPath); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if progress == nil {
|
||||||
|
progress = func(CopyProgress) {}
|
||||||
|
}
|
||||||
|
if stats.FilesTotal == 0 && stats.BytesTotal == 0 {
|
||||||
|
resolved, err := CopyStats(srcPath)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
stats = resolved
|
||||||
|
}
|
||||||
|
tracker := copyProgressState{ctx: ctx, stats: stats, callback: progress}
|
||||||
|
tracker.emit(srcPath, true)
|
||||||
|
|
||||||
|
cleanupOnErr := func(copyErr error) (string, error) {
|
||||||
|
if copyErr != nil {
|
||||||
|
_ = os.RemoveAll(targetPath)
|
||||||
|
}
|
||||||
|
return "", copyErr
|
||||||
|
}
|
||||||
|
|
||||||
|
if srcInfo.Mode()&os.ModeSymlink != 0 {
|
||||||
|
target, err := os.Readlink(srcPath)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if err := os.Symlink(target, targetPath); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
tracker.finishFile(srcPath)
|
||||||
|
return targetPath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if srcInfo.IsDir() {
|
||||||
|
if err := copyDir(srcPath, targetPath, &tracker); err != nil {
|
||||||
|
return cleanupOnErr(err)
|
||||||
|
}
|
||||||
|
tracker.emit(srcPath, true)
|
||||||
|
return targetPath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := copyFile(srcPath, targetPath, srcInfo.Mode(), &tracker); err != nil {
|
||||||
|
return cleanupOnErr(err)
|
||||||
|
}
|
||||||
|
tracker.emit(srcPath, true)
|
||||||
|
return targetPath, nil
|
||||||
|
}
|
||||||
|
|
||||||
func CopyStats(srcPath string) (TransferStats, error) {
|
func CopyStats(srcPath string) (TransferStats, error) {
|
||||||
srcInfo, err := os.Lstat(srcPath)
|
srcInfo, err := os.Lstat(srcPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -77,67 +160,7 @@ func CopyStats(srcPath string) (TransferStats, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func CopyPathWithProgress(srcPath string, dstDir string, overwrite bool, stats TransferStats, progress func(CopyProgress)) (string, error) {
|
func CopyPathWithProgress(srcPath string, dstDir string, overwrite bool, stats TransferStats, progress func(CopyProgress)) (string, error) {
|
||||||
srcInfo, err := os.Lstat(srcPath)
|
return CopyPathWithProgressContext(context.Background(), srcPath, dstDir, overwrite, stats, progress)
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("stat %s: %w", srcPath, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
targetPath := filepath.Join(dstDir, filepath.Base(srcPath))
|
|
||||||
if same, err := samePath(srcPath, targetPath); err != nil {
|
|
||||||
return "", err
|
|
||||||
} else if same {
|
|
||||||
return "", fmt.Errorf("source and target are the same: %s", targetPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
if exists, err := PathExists(targetPath); err != nil {
|
|
||||||
return "", err
|
|
||||||
} else if exists {
|
|
||||||
if !overwrite {
|
|
||||||
return "", ErrOverwrite(targetPath)
|
|
||||||
}
|
|
||||||
if err := os.RemoveAll(targetPath); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if progress == nil {
|
|
||||||
progress = func(CopyProgress) {}
|
|
||||||
}
|
|
||||||
if stats.FilesTotal == 0 && stats.BytesTotal == 0 {
|
|
||||||
resolved, err := CopyStats(srcPath)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
stats = resolved
|
|
||||||
}
|
|
||||||
tracker := copyProgressState{stats: stats, callback: progress}
|
|
||||||
tracker.emit(srcPath, true)
|
|
||||||
|
|
||||||
if srcInfo.Mode()&os.ModeSymlink != 0 {
|
|
||||||
target, err := os.Readlink(srcPath)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
if err := os.Symlink(target, targetPath); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
tracker.finishFile(srcPath)
|
|
||||||
return targetPath, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if srcInfo.IsDir() {
|
|
||||||
if err := copyDir(srcPath, targetPath, &tracker); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
tracker.emit(srcPath, true)
|
|
||||||
return targetPath, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := copyFile(srcPath, targetPath, srcInfo.Mode(), &tracker); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
tracker.emit(srcPath, true)
|
|
||||||
return targetPath, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func MovePath(srcPath string, dstDir string, overwrite bool) (string, error) {
|
func MovePath(srcPath string, dstDir string, overwrite bool) (string, error) {
|
||||||
|
|
@ -145,6 +168,14 @@ func MovePath(srcPath string, dstDir string, overwrite bool) (string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func MovePathWithProgress(srcPath string, dstDir string, overwrite bool, stats TransferStats, progress func(CopyProgress)) (string, error) {
|
func MovePathWithProgress(srcPath string, dstDir string, overwrite bool, stats TransferStats, progress func(CopyProgress)) (string, error) {
|
||||||
|
return MovePathWithProgressContext(context.Background(), srcPath, dstDir, overwrite, stats, progress)
|
||||||
|
}
|
||||||
|
|
||||||
|
func MovePathWithProgressContext(ctx context.Context, srcPath string, dstDir string, overwrite bool, stats TransferStats, progress func(CopyProgress)) (string, error) {
|
||||||
|
if ctx == nil {
|
||||||
|
ctx = context.Background()
|
||||||
|
}
|
||||||
|
|
||||||
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
|
||||||
|
|
@ -166,6 +197,9 @@ func MovePathWithProgress(srcPath string, dstDir string, overwrite bool, stats T
|
||||||
if progress == nil {
|
if progress == nil {
|
||||||
progress = func(CopyProgress) {}
|
progress = func(CopyProgress) {}
|
||||||
}
|
}
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
if stats.FilesTotal == 0 && stats.BytesTotal == 0 {
|
if stats.FilesTotal == 0 && stats.BytesTotal == 0 {
|
||||||
resolved, err := CopyStats(srcPath)
|
resolved, err := CopyStats(srcPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -187,10 +221,14 @@ func MovePathWithProgress(srcPath string, dstDir string, overwrite bool, stats T
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
targetPath, err := CopyPathWithProgress(srcPath, dstDir, overwrite, stats, progress)
|
targetPath, err := CopyPathWithProgressContext(ctx, srcPath, dstDir, overwrite, stats, progress)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
_ = os.RemoveAll(targetPath)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
if err := DeletePath(srcPath); err != nil {
|
if err := DeletePath(srcPath); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
@ -220,6 +258,11 @@ func MakeDir(parent string, name string) (string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func copyDir(srcDir string, dstDir string, tracker *copyProgressState) error {
|
func copyDir(srcDir string, dstDir string, tracker *copyProgressState) error {
|
||||||
|
if tracker != nil && tracker.ctx != nil {
|
||||||
|
if err := tracker.ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
info, err := os.Lstat(srcDir)
|
info, err := os.Lstat(srcDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -234,6 +277,11 @@ func copyDir(srcDir string, dstDir string, tracker *copyProgressState) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
|
if tracker != nil && tracker.ctx != nil {
|
||||||
|
if err := tracker.ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
srcPath := filepath.Join(srcDir, entry.Name())
|
srcPath := filepath.Join(srcDir, entry.Name())
|
||||||
dstPath := filepath.Join(dstDir, entry.Name())
|
dstPath := filepath.Join(dstDir, entry.Name())
|
||||||
|
|
||||||
|
|
@ -269,6 +317,11 @@ func copyDir(srcDir string, dstDir string, tracker *copyProgressState) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func copyFile(srcPath string, dstPath string, mode os.FileMode, tracker *copyProgressState) error {
|
func copyFile(srcPath string, dstPath string, mode os.FileMode, tracker *copyProgressState) error {
|
||||||
|
if tracker != nil && tracker.ctx != nil {
|
||||||
|
if err := tracker.ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
srcFile, err := os.Open(srcPath)
|
srcFile, err := os.Open(srcPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -286,6 +339,8 @@ func copyFile(srcPath string, dstPath string, mode os.FileMode, tracker *copyPro
|
||||||
writer = &progressWriter{base: dstFile, tracker: tracker, path: srcPath}
|
writer = &progressWriter{base: dstFile, tracker: tracker, path: srcPath}
|
||||||
}
|
}
|
||||||
if _, err := io.Copy(writer, srcFile); err != nil {
|
if _, err := io.Copy(writer, srcFile); err != nil {
|
||||||
|
_ = dstFile.Close()
|
||||||
|
_ = os.Remove(dstPath)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if tracker != nil {
|
if tracker != nil {
|
||||||
|
|
@ -301,6 +356,11 @@ type progressWriter struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *progressWriter) Write(data []byte) (int, error) {
|
func (w *progressWriter) Write(data []byte) (int, error) {
|
||||||
|
if w.tracker != nil && w.tracker.ctx != nil {
|
||||||
|
if err := w.tracker.ctx.Err(); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
}
|
||||||
n, err := w.base.Write(data)
|
n, err := w.base.Write(data)
|
||||||
if n > 0 {
|
if n > 0 {
|
||||||
w.tracker.addBytes(int64(n), w.path)
|
w.tracker.addBytes(int64(n), w.path)
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ type KeyMap struct {
|
||||||
Delete key.Binding
|
Delete key.Binding
|
||||||
Confirm key.Binding
|
Confirm key.Binding
|
||||||
Background key.Binding
|
Background key.Binding
|
||||||
|
ProgressCancel key.Binding
|
||||||
Cancel key.Binding
|
Cancel key.Binding
|
||||||
Quit key.Binding
|
Quit key.Binding
|
||||||
}
|
}
|
||||||
|
|
@ -59,6 +60,7 @@ func DefaultKeyMap() KeyMap {
|
||||||
Delete: key.NewBinding(key.WithKeys("f8", "delete", "x"), key.WithHelp("F8/x", "delete")),
|
Delete: key.NewBinding(key.WithKeys("f8", "delete", "x"), key.WithHelp("F8/x", "delete")),
|
||||||
Confirm: key.NewBinding(key.WithKeys("enter", "y"), key.WithHelp("Enter/y", "confirm")),
|
Confirm: key.NewBinding(key.WithKeys("enter", "y"), key.WithHelp("Enter/y", "confirm")),
|
||||||
Background: key.NewBinding(key.WithKeys("b"), key.WithHelp("b", "background")),
|
Background: key.NewBinding(key.WithKeys("b"), key.WithHelp("b", "background")),
|
||||||
|
ProgressCancel: key.NewBinding(key.WithKeys("c"), key.WithHelp("c", "cancel transfer")),
|
||||||
Cancel: key.NewBinding(key.WithKeys("esc", "n"), key.WithHelp("Esc/n", "cancel")),
|
Cancel: key.NewBinding(key.WithKeys("esc", "n"), key.WithHelp("Esc/n", "cancel")),
|
||||||
Quit: key.NewBinding(key.WithKeys("f10", "q", "ctrl+c"), key.WithHelp("F10/q", "quit")),
|
Quit: key.NewBinding(key.WithKeys("f10", "q", "ctrl+c"), key.WithHelp("F10/q", "quit")),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package ui
|
package ui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
|
@ -117,6 +118,8 @@ type copyJobState struct {
|
||||||
progress vfs.CopyProgress
|
progress vfs.CopyProgress
|
||||||
overwrite bool
|
overwrite bool
|
||||||
background bool
|
background bool
|
||||||
|
cancel context.CancelFunc
|
||||||
|
startedAt time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
type mouseClickState struct {
|
type mouseClickState struct {
|
||||||
|
|
@ -334,7 +337,11 @@ 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 {
|
||||||
|
if msg.err == context.Canceled {
|
||||||
|
m.status = strings.Title(operationVerb(msg.kind)) + " cancelled"
|
||||||
|
} else {
|
||||||
m.status = fmt.Sprintf("%s failed: %v", strings.Title(operationVerb(msg.kind)), 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{}
|
||||||
|
|
@ -584,16 +591,6 @@ func (m Model) handleModalKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||||
}
|
}
|
||||||
|
|
||||||
case modalCopyProgress:
|
case modalCopyProgress:
|
||||||
if isModalCloseKey(msg, m.keys) {
|
|
||||||
if m.copyJob == nil {
|
|
||||||
m.modal = modalState{}
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
m.copyJob.background = true
|
|
||||||
m.modal = modalState{}
|
|
||||||
m.status = "Transfer continues in background"
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
if key.Matches(msg, m.keys.Background) {
|
if key.Matches(msg, m.keys.Background) {
|
||||||
if m.copyJob == nil {
|
if m.copyJob == nil {
|
||||||
m.modal = modalState{}
|
m.modal = modalState{}
|
||||||
|
|
@ -604,6 +601,17 @@ func (m Model) handleModalKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||||
m.status = "Transfer continues in background"
|
m.status = "Transfer continues in background"
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
if key.Matches(msg, m.keys.ProgressCancel) {
|
||||||
|
if m.copyJob == nil {
|
||||||
|
m.modal = modalState{}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
if m.copyJob.cancel != nil {
|
||||||
|
m.copyJob.cancel()
|
||||||
|
}
|
||||||
|
m.status = strings.Title(operationVerb(m.copyJob.kind)) + " cancelling..."
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
|
|
||||||
case modalNotice:
|
case modalNotice:
|
||||||
|
|
@ -1120,6 +1128,7 @@ func (m *Model) openHelpModal() {
|
||||||
" Enter / y confirm action",
|
" Enter / y confirm action",
|
||||||
" Esc / n cancel action",
|
" Esc / n cancel action",
|
||||||
" b run copy/move in background (progress dialog)",
|
" b run copy/move in background (progress dialog)",
|
||||||
|
" c cancel active copy/move transfer",
|
||||||
" F5/F6/F8 apply to marked entries when selection exists",
|
" F5/F6/F8 apply to marked entries when selection exists",
|
||||||
"",
|
"",
|
||||||
"Mouse",
|
"Mouse",
|
||||||
|
|
@ -1711,19 +1720,19 @@ func renderCopyProgressModal(job copyJobState, palette theme.Palette, width int)
|
||||||
|
|
||||||
lines := []string{
|
lines := []string{
|
||||||
titleStyle.Render(progressTitle(job.kind)),
|
titleStyle.Render(progressTitle(job.kind)),
|
||||||
lineStyle.Render(fmt.Sprintf("From: %s", transferSourceLabel(job.sourcePaths))),
|
|
||||||
lineStyle.Render(fmt.Sprintf("To: %s", job.targetDir)),
|
|
||||||
spacer,
|
spacer,
|
||||||
lineStyle.Render(renderProgressBar(ratio, max(contentWidth-8, 10), palette)),
|
lineStyle.Render(renderProgressBar(ratio, max(contentWidth-8, 10), palette)),
|
||||||
lineStyle.Render(fmt.Sprintf("Files: %d / %d", progress.FilesDone, progress.FilesTotal)),
|
spacer,
|
||||||
lineStyle.Render(fmt.Sprintf("Size: %s / %s", formatSize(progress.BytesDone, true), formatSize(progress.BytesTotal, true))),
|
renderProgressPercentLine(ratio, contentWidth, palette),
|
||||||
|
renderProgressStatLine("Files:", fmt.Sprintf("%d / %d", progress.FilesDone, progress.FilesTotal), contentWidth, palette),
|
||||||
|
renderProgressStatLine("Size:", fmt.Sprintf("%s / %s", formatSize(progress.BytesDone, true), formatSize(progress.BytesTotal, true)), contentWidth, palette),
|
||||||
|
renderProgressStatLine("Speed:", transferSpeed(progress.BytesDone, job.startedAt), contentWidth, palette),
|
||||||
|
spacer,
|
||||||
|
renderProgressActions(contentWidth, palette),
|
||||||
}
|
}
|
||||||
|
if job.background {
|
||||||
if strings.TrimSpace(progress.CurrentPath) != "" {
|
lines = append(lines, mutedStyle.Render("Transfer continues in background"))
|
||||||
lines = append(lines, lineStyle.Render("Current: "+truncateMiddle(progress.CurrentPath, max(contentWidth-10, 16))))
|
|
||||||
}
|
}
|
||||||
lines = append(lines, spacer)
|
|
||||||
lines = append(lines, mutedStyle.Render("Press b to continue in background"))
|
|
||||||
|
|
||||||
return box.Render(strings.Join(lines, "\n"))
|
return box.Render(strings.Join(lines, "\n"))
|
||||||
}
|
}
|
||||||
|
|
@ -1748,8 +1757,97 @@ func renderProgressBar(ratio float64, width int, palette theme.Palette) string {
|
||||||
|
|
||||||
bar := lipgloss.NewStyle().Foreground(palette.ProgressFill).Render(strings.Repeat("█", filled))
|
bar := lipgloss.NewStyle().Foreground(palette.ProgressFill).Render(strings.Repeat("█", filled))
|
||||||
rest := lipgloss.NewStyle().Foreground(palette.ProgressEmpty).Render(strings.Repeat("░", width-filled))
|
rest := lipgloss.NewStyle().Foreground(palette.ProgressEmpty).Render(strings.Repeat("░", width-filled))
|
||||||
percent := fmt.Sprintf(" %3.0f%%", ratio*100)
|
return bar + rest
|
||||||
return bar + rest + percent
|
}
|
||||||
|
|
||||||
|
func renderProgressStatLine(label string, value string, width int, palette theme.Palette) string {
|
||||||
|
keyWidth := min(max(lipgloss.Width(label)+1, 8), width)
|
||||||
|
valueWidth := max(width-keyWidth, 0)
|
||||||
|
keyStyle := lipgloss.NewStyle().
|
||||||
|
Width(keyWidth).
|
||||||
|
Background(palette.Panel).
|
||||||
|
Foreground(palette.FooterKey).
|
||||||
|
Bold(true)
|
||||||
|
valueStyle := lipgloss.NewStyle().
|
||||||
|
Width(valueWidth).
|
||||||
|
Background(palette.Panel).
|
||||||
|
Foreground(palette.Text)
|
||||||
|
return keyStyle.Render(label) + valueStyle.Render(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderProgressActions(width int, palette theme.Palette) string {
|
||||||
|
const minButtonWidth = 14
|
||||||
|
const maxButtonWidth = 18
|
||||||
|
const gapWidth = 4
|
||||||
|
labelWidth := max(lipgloss.Width("Background / b"), lipgloss.Width("Cancel / c"))
|
||||||
|
buttonWidth := min(max(labelWidth+2, minButtonWidth), maxButtonWidth)
|
||||||
|
buttonWidth = min(buttonWidth, max((width-gapWidth)/2, labelWidth))
|
||||||
|
|
||||||
|
backgroundBtn := lipgloss.NewStyle().
|
||||||
|
Width(buttonWidth).
|
||||||
|
Align(lipgloss.Center).
|
||||||
|
Background(palette.Info).
|
||||||
|
Foreground(palette.Background).
|
||||||
|
Bold(true).
|
||||||
|
Render("Background / b")
|
||||||
|
cancelBtn := lipgloss.NewStyle().
|
||||||
|
Width(buttonWidth).
|
||||||
|
Align(lipgloss.Center).
|
||||||
|
Background(palette.CancelButton).
|
||||||
|
Foreground(palette.Background).
|
||||||
|
Bold(true).
|
||||||
|
Render("Cancel / c")
|
||||||
|
|
||||||
|
gap := lipgloss.NewStyle().
|
||||||
|
Width(gapWidth).
|
||||||
|
Background(palette.Panel).
|
||||||
|
Render("")
|
||||||
|
backgroundBias := lipgloss.NewStyle().
|
||||||
|
Width(10).
|
||||||
|
Background(palette.Panel).
|
||||||
|
Render("")
|
||||||
|
cancelBias := lipgloss.NewStyle().
|
||||||
|
Width(4).
|
||||||
|
Background(palette.Panel).
|
||||||
|
Render("")
|
||||||
|
group := lipgloss.JoinHorizontal(lipgloss.Top, backgroundBtn, backgroundBias, gap, cancelBtn, cancelBias)
|
||||||
|
row := lipgloss.PlaceHorizontal(
|
||||||
|
width,
|
||||||
|
lipgloss.Center,
|
||||||
|
group,
|
||||||
|
lipgloss.WithWhitespaceBackground(palette.Panel),
|
||||||
|
)
|
||||||
|
return lipgloss.NewStyle().
|
||||||
|
Width(width).
|
||||||
|
Background(palette.Panel).
|
||||||
|
Render(row)
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderProgressPercentLine(ratio float64, width int, palette theme.Palette) string {
|
||||||
|
percent := lipgloss.NewStyle().
|
||||||
|
Foreground(palette.Info).
|
||||||
|
Bold(true).
|
||||||
|
Render(fmt.Sprintf("%3.0f%%", ratio*100))
|
||||||
|
return lipgloss.NewStyle().
|
||||||
|
Width(width).
|
||||||
|
Background(palette.Panel).
|
||||||
|
Align(lipgloss.Right).
|
||||||
|
Render(percent)
|
||||||
|
}
|
||||||
|
|
||||||
|
func transferSpeed(bytesDone int64, startedAt time.Time) string {
|
||||||
|
if startedAt.IsZero() || bytesDone <= 0 {
|
||||||
|
return "calculating..."
|
||||||
|
}
|
||||||
|
elapsed := time.Since(startedAt)
|
||||||
|
if elapsed <= 0 {
|
||||||
|
return "calculating..."
|
||||||
|
}
|
||||||
|
perSecond := int64(float64(bytesDone) / elapsed.Seconds())
|
||||||
|
if perSecond <= 0 {
|
||||||
|
return "calculating..."
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s/s", vfs.HumanSize(perSecond))
|
||||||
}
|
}
|
||||||
|
|
||||||
func overlayCenter(base string, overlay string, width int) string {
|
func overlayCenter(base string, overlay string, width int) string {
|
||||||
|
|
@ -1913,6 +2011,7 @@ func dismissNoticeCmd(delay time.Duration) tea.Cmd {
|
||||||
func (m *Model) startCopyJob(kind fileOpKind, sourcePaths []string, targetDir string, overwrite bool, stats vfs.TransferStats) tea.Cmd {
|
func (m *Model) startCopyJob(kind fileOpKind, sourcePaths []string, targetDir string, overwrite bool, stats vfs.TransferStats) tea.Cmd {
|
||||||
m.nextCopyJob++
|
m.nextCopyJob++
|
||||||
jobID := m.nextCopyJob
|
jobID := m.nextCopyJob
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
m.copyJob = ©JobState{
|
m.copyJob = ©JobState{
|
||||||
id: jobID,
|
id: jobID,
|
||||||
kind: kind,
|
kind: kind,
|
||||||
|
|
@ -1926,6 +2025,8 @@ func (m *Model) startCopyJob(kind fileOpKind, sourcePaths []string, targetDir st
|
||||||
BytesTotal: stats.BytesTotal,
|
BytesTotal: stats.BytesTotal,
|
||||||
CurrentPath: sourcePaths[0],
|
CurrentPath: sourcePaths[0],
|
||||||
},
|
},
|
||||||
|
cancel: cancel,
|
||||||
|
startedAt: time.Now(),
|
||||||
}
|
}
|
||||||
m.modal = modalState{kind: modalCopyProgress}
|
m.modal = modalState{kind: modalCopyProgress}
|
||||||
m.status = strings.Title(operationVerb(kind)) + " started"
|
m.status = strings.Title(operationVerb(kind)) + " started"
|
||||||
|
|
@ -1961,9 +2062,9 @@ func (m *Model) startCopyJob(kind fileOpKind, sourcePaths []string, targetDir st
|
||||||
}
|
}
|
||||||
switch kind {
|
switch kind {
|
||||||
case opMove:
|
case opMove:
|
||||||
_, statErr = vfs.MovePathWithProgress(sourcePath, targetDir, overwrite, entryStats, progressFn)
|
_, statErr = vfs.MovePathWithProgressContext(ctx, sourcePath, targetDir, overwrite, entryStats, progressFn)
|
||||||
default:
|
default:
|
||||||
_, statErr = vfs.CopyPathWithProgress(sourcePath, targetDir, overwrite, entryStats, progressFn)
|
_, statErr = vfs.CopyPathWithProgressContext(ctx, sourcePath, targetDir, overwrite, entryStats, progressFn)
|
||||||
}
|
}
|
||||||
if statErr != nil {
|
if statErr != nil {
|
||||||
m.copyProgress <- copyDoneMsg{
|
m.copyProgress <- copyDoneMsg{
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue