feat: SFTP file transfer progress bar + auto-refresh

- Progress bar: [████████░░░░] 67% 12.3MB/18.5MB
- progressWriter wraps io.Writer, reports bytes via callback
- Footer shows live progress during upload/download
- Auto-refresh destination pane after transfer completes
- Success message shown for 1 second before clearing
This commit is contained in:
swanadiva
2026-06-24 14:25:11 +07:00
parent a597ea5291
commit 6b95385094
2 changed files with 74 additions and 10 deletions
+67 -10
View File
@@ -425,27 +425,59 @@ func (t *SFTPBrowserTab) Update(msg tea.Msg) (Tab, tea.Cmd) {
}
func (t *SFTPBrowserTab) copyFile(srcType, dstType paneType, srcPath, dstPath, name string) {
// Get file size for progress bar
var totalSize int64
if srcType == paneRemote {
t.mu.Lock()
client := t.right.sftpClient
t.mu.Unlock()
if client != nil {
if info, err := client.Stat(srcPath); err == nil {
totalSize = info.Size()
}
}
} else {
if info, err := os.Stat(srcPath); err == nil {
totalSize = info.Size()
}
}
t.mu.Lock()
t.transferring = true
t.transferMsg = fmt.Sprintf("Copying %s...", name)
t.transferMsg = fmt.Sprintf("Copying %s... 0%%", name)
t.mu.Unlock()
progressFn := func(transferred int64) {
if totalSize <= 0 {
return
}
pct := int(transferred * 100 / totalSize)
barWidth := 20
filled := int(float64(barWidth) * float64(transferred) / float64(totalSize))
if filled > barWidth {
filled = barWidth
}
bar := strings.Repeat("█", filled) + strings.Repeat("░", barWidth-filled)
t.mu.Lock()
t.transferMsg = fmt.Sprintf("Copying %s... [%s] %d%% %s/%s",
name, bar, pct, formatSize(transferred), formatSize(totalSize))
t.mu.Unlock()
}
var err error
if srcType == paneRemote && dstType == paneLocal {
// Download
t.mu.Lock()
client := t.right.sftpClient
t.mu.Unlock()
if client != nil {
err = downloadFile(client, srcPath, dstPath)
err = downloadFile(client, srcPath, dstPath, progressFn)
}
} else if srcType == paneLocal && dstType == paneRemote {
// Upload
t.mu.Lock()
client := t.right.sftpClient
t.mu.Unlock()
if client != nil {
err = uploadFile(client, srcPath, dstPath)
err = uploadFile(client, srcPath, dstPath, progressFn)
}
}
@@ -453,19 +485,26 @@ func (t *SFTPBrowserTab) copyFile(srcType, dstType paneType, srcPath, dstPath, n
if err != nil {
t.err = fmt.Errorf("copy %s: %w", name, err)
} else {
t.transferMsg = ""
t.transferMsg = fmt.Sprintf("Done: %s", name)
}
t.transferring = false
t.mu.Unlock()
// Auto-refresh destination pane
if dstType == paneRemote {
t.refreshRemote()
} else {
t.refreshLocal()
}
// Clear success message after brief delay
time.Sleep(1 * time.Second)
t.mu.Lock()
t.transferMsg = ""
t.mu.Unlock()
}
func downloadFile(client *sftp.Client, remotePath, localPath string) error {
func downloadFile(client *sftp.Client, remotePath, localPath string, progressFn func(int64)) error {
src, err := client.Open(remotePath)
if err != nil {
return err
@@ -478,11 +517,12 @@ func downloadFile(client *sftp.Client, remotePath, localPath string) error {
}
defer dst.Close()
_, err = io.Copy(dst, src)
pw := &progressWriter{w: dst, total: 0, fn: progressFn}
_, err = io.Copy(pw, src)
return err
}
func uploadFile(client *sftp.Client, localPath, remotePath string) error {
func uploadFile(client *sftp.Client, localPath, remotePath string, progressFn func(int64)) error {
src, err := os.Open(localPath)
if err != nil {
return err
@@ -495,10 +535,27 @@ func uploadFile(client *sftp.Client, localPath, remotePath string) error {
}
defer dst.Close()
_, err = io.Copy(dst, src)
pw := &progressWriter{w: dst, total: 0, fn: progressFn}
_, err = io.Copy(pw, src)
return err
}
// progressWriter wraps an io.Writer and reports progress via callback
type progressWriter struct {
w io.Writer
total int64
fn func(int64)
}
func (pw *progressWriter) Write(p []byte) (int, error) {
n, err := pw.w.Write(p)
pw.total += int64(n)
if pw.fn != nil {
pw.fn(pw.total)
}
return n, err
}
func (t *SFTPBrowserTab) deleteItem(fullPath string, isDir bool, isRemote bool) {
t.mu.Lock()
t.transferring = true