From fdb09f1c9d9644b31f8aef77b3bb0a10bdd8b8d7 Mon Sep 17 00:00:00 2001 From: Rose Hogenson Date: Thu, 17 Apr 2025 20:44:41 -0700 Subject: Handle lines longer than terminal width --- ccp.go | 40 ++++++++++++++++++++++++++------- internal/render/render.go | 57 +++++++++++++++++++++++++++++++++++++---------- 2 files changed, 77 insertions(+), 20 deletions(-) diff --git a/ccp.go b/ccp.go index a7082e4..94eac96 100644 --- a/ccp.go +++ b/ccp.go @@ -7,9 +7,11 @@ import ( "flag" "fmt" "os" + "path/filepath" "strings" "sync" "time" + "unicode/utf8" "github.com/charmbracelet/bubbles/progress" "github.com/charmbracelet/lipgloss" @@ -33,9 +35,10 @@ type measurement struct { // progressUpdater implements the cp.Progress interface. type progressUpdater struct { mu sync.Mutex - max int64 // Total bytes to copy - current int64 // Current bytes copied - copyingFile string // File currently being copied + max int64 // Total bytes to copy + current int64 // Current bytes copied + copyingFrom string // File currently being copied + copyingTo string errs []error // Any errors encountered } @@ -51,11 +54,21 @@ func (pu *progressUpdater) Progress(n int64) { pu.current += n } +func abbreviatePath(p string) string { + parts := strings.Split(p, string(filepath.Separator)) + for i := 1; i < len(parts)-1; i++ { + part := parts[i] + _, n := utf8.DecodeRuneInString(part) + parts[i] = part[:n] + } + return strings.Join(parts, string(filepath.Separator)) +} + func (pu *progressUpdater) FileStart(from, to string) { - s := from + " -> " + to pu.mu.Lock() defer pu.mu.Unlock() - pu.copyingFile = s + pu.copyingFrom = from + pu.copyingTo = to } func (pu *progressUpdater) Error(err error) { @@ -163,16 +176,27 @@ func run() error { currentProgress.mu.Lock() current := currentProgress.current max := currentProgress.max - copyingFile := currentProgress.copyingFile + copyingFrom := currentProgress.copyingFrom + copyingTo := currentProgress.copyingTo errs := currentProgress.errs currentProgress.mu.Unlock() - renderer.Clear() + renderer.Clear(width) + copyingFile := "" + if copyingFrom != "" { + copyingFile = copyingFrom + " -> " + copyingTo + if len(copyingFile)+4 > width { + copyingFile = copyingFrom + " -> " + abbreviatePath(copyingTo) + if len(copyingFile)+4 > width { + copyingFile = abbreviatePath(copyingFrom) + " -> " + abbreviatePath(copyingTo) + } + } + } progress := 0. if max > 0 { progress = float64(current) / float64(max) } - etaStr := "calculating..." + etaStr := "..." if eta >= 0 { etaStr = eta.Round(time.Second).String() } diff --git a/internal/render/render.go b/internal/render/render.go index 877e2bd..18708f3 100644 --- a/internal/render/render.go +++ b/internal/render/render.go @@ -6,6 +6,7 @@ import ( "bytes" "fmt" "os" + "unicode/utf8" ) // Renderer updates a terminal UI. Typical usage looks like @@ -20,8 +21,10 @@ import ( // r.Flush() // } type Renderer struct { - w bufio.Writer - prevLines int + w bufio.Writer + prevLines int + width int + partialLineLen int } // New creates a new Renderer @@ -32,12 +35,38 @@ func New() *Renderer { } // Clear clears the screen before rendering a new frame. -func (r *Renderer) Clear() { +func (r *Renderer) Clear(width int) { + r.width = width if r.prevLines > 0 { fmt.Fprintf(&r.w, "\033[%dA", r.prevLines) } - r.prevLines = 0 r.w.WriteString("\r") + r.prevLines = 0 + r.partialLineLen = 0 +} + +func truncate(b []byte, width int) ([]byte, int) { + n := 0 + for i := 0; i < len(b); { + if bytes.HasPrefix(b[i:], []byte("\033[")) { + // An escape sequence usually starts with [, then has one or two numbers + // separated by semicolon, and ends with some terminating character. To + // try and munch the whole sequence, skip over any numbers and + // semicolon here. + for i += 2; i < len(b)-1 && ('0' <= b[i] && b[i] <= '9' || b[i] == ';'); i++ { + } + // Skip the terminating character. + i++ + continue + } + if n+1 > width { + return b[:i], n + } + _, runeWidth := utf8.DecodeRune(b[i:]) + i += runeWidth + n++ + } + return b, n } // Write implements io.Writer. @@ -46,22 +75,26 @@ func (r *Renderer) Write(buf []byte) (int, error) { for len(buf) > 0 { i := bytes.IndexByte(buf, '\n') if i < 0 { - n, err := r.w.Write(buf) - totalBytes += n - return totalBytes, err + line, lineWidth := truncate(buf, r.width-r.partialLineLen) + r.partialLineLen += lineWidth + if n, err := r.w.Write(line); err != nil { + return totalBytes + n, err + } + totalBytes += len(buf) + return totalBytes, nil } - line := buf[:i] + line, _ := truncate(buf[:i], r.width) buf = buf[i+1:] - n, err := r.w.Write(line) - totalBytes += n - if err != nil { - return totalBytes, err + if n, err := r.w.Write(line); err != nil { + return totalBytes + n, err } + totalBytes += i if _, err := r.w.WriteString("\033[K\n"); err != nil { return totalBytes, err } totalBytes++ r.prevLines++ + r.partialLineLen = 0 } return totalBytes, nil } -- cgit v1.3.1