restic/internal/ui/format.go
Florian Thoma e9de9684f4
Use character display width for table padding
Using len(...) for table cell padding produced wrong results for unicode
chracters leading to misaligned tables. Implementation changed to take
the actual terminal display width into consideration.
2024-06-05 09:33:15 +02:00

131 lines
2.7 KiB
Go

package ui
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"math/bits"
"strconv"
"time"
"golang.org/x/text/width"
)
func FormatBytes(c uint64) string {
b := float64(c)
switch {
case c >= 1<<40:
return fmt.Sprintf("%.3f TiB", b/(1<<40))
case c >= 1<<30:
return fmt.Sprintf("%.3f GiB", b/(1<<30))
case c >= 1<<20:
return fmt.Sprintf("%.3f MiB", b/(1<<20))
case c >= 1<<10:
return fmt.Sprintf("%.3f KiB", b/(1<<10))
default:
return fmt.Sprintf("%d B", c)
}
}
// FormatPercent formats numerator/denominator as a percentage.
func FormatPercent(numerator uint64, denominator uint64) string {
if denominator == 0 {
return ""
}
percent := 100.0 * float64(numerator) / float64(denominator)
if percent > 100 {
percent = 100
}
return fmt.Sprintf("%3.2f%%", percent)
}
// FormatDuration formats d as FormatSeconds would.
func FormatDuration(d time.Duration) string {
sec := uint64(d / time.Second)
return FormatSeconds(sec)
}
// FormatSeconds formats sec as MM:SS, or HH:MM:SS if sec seconds
// is at least an hour.
func FormatSeconds(sec uint64) string {
hours := sec / 3600
sec -= hours * 3600
min := sec / 60
sec -= min * 60
if hours > 0 {
return fmt.Sprintf("%d:%02d:%02d", hours, min, sec)
}
return fmt.Sprintf("%d:%02d", min, sec)
}
// ParseBytes parses a size in bytes from s. It understands the suffixes
// B, K, M, G and T for powers of 1024.
func ParseBytes(s string) (int64, error) {
if s == "" {
return 0, errors.New("expected size, got empty string")
}
numStr := s[:len(s)-1]
var unit uint64 = 1
switch s[len(s)-1] {
case 'b', 'B':
// use initialized values, do nothing here
case 'k', 'K':
unit = 1024
case 'm', 'M':
unit = 1024 * 1024
case 'g', 'G':
unit = 1024 * 1024 * 1024
case 't', 'T':
unit = 1024 * 1024 * 1024 * 1024
default:
numStr = s
}
value, err := strconv.ParseInt(numStr, 10, 64)
if err != nil {
return 0, err
}
hi, lo := bits.Mul64(uint64(value), unit)
value = int64(lo)
if hi != 0 || value < 0 {
return 0, fmt.Errorf("ParseSize: %q: %w", numStr, strconv.ErrRange)
}
return value, nil
}
func ToJSONString(status interface{}) string {
buf := new(bytes.Buffer)
err := json.NewEncoder(buf).Encode(status)
if err != nil {
panic(err)
}
return buf.String()
}
// TerminalDisplayWidth returns the number of terminal cells needed to display s
func TerminalDisplayWidth(s string) int {
width := 0
for _, r := range s {
width += terminalDisplayRuneWidth(r)
}
return width
}
func terminalDisplayRuneWidth(r rune) int {
switch width.LookupRune(r).Kind() {
case width.EastAsianWide, width.EastAsianFullwidth:
return 2
case width.EastAsianNarrow, width.EastAsianHalfwidth, width.EastAsianAmbiguous, width.Neutral:
return 1
default:
return 0
}
}