unarr/internal/ui/format.go
Deivid Soto 29cf0a0126 feat: initial commit — unarr CLI
Search, inspect, stream, and download torrents from the terminal.
Replaces the entire *arr stack with a single binary.
2026-03-28 11:29:42 +01:00

191 lines
3.7 KiB
Go

package ui
import (
"fmt"
"math"
"strconv"
"strings"
"time"
)
// FormatSize converts bytes to human-readable format.
func FormatSize(sizeBytes *int64) string {
if sizeBytes == nil {
return "?"
}
return FormatBytes(*sizeBytes)
}
// FormatBytes converts bytes to human-readable format.
func FormatBytes(b int64) string {
if b == 0 {
return "0 B"
}
const unit = 1024
if b < unit {
return fmt.Sprintf("%d B", b)
}
div, exp := int64(unit), 0
for n := b / unit; n >= unit; n /= unit {
div *= unit
exp++
}
val := float64(b) / float64(div)
units := []string{"KB", "MB", "GB", "TB"}
if exp >= len(units) {
exp = len(units) - 1
}
return fmt.Sprintf("%.1f %s", val, units[exp])
}
// QualityIndicator returns a colored emoji for quality score (0-100 scale).
func QualityIndicator(score *int) string {
if score == nil {
return " "
}
s := *score
switch {
case s >= 70:
return "🟢"
case s >= 40:
return "🟡"
default:
return "🔴"
}
}
// SeedHealthIndicator returns a colored emoji for seed count.
func SeedHealthIndicator(seeds int) string {
switch {
case seeds > 100:
return "🟢"
case seeds >= 10:
return "🟡"
default:
return "🔴"
}
}
// FormatRating returns a display string for a rating.
func FormatRating(rating *string) string {
if rating == nil {
return "-"
}
return *rating
}
// FormatYear returns a display string for a year.
func FormatYear(year *int) string {
if year == nil {
return "-"
}
return strconv.Itoa(*year)
}
// FormatContentType returns a short display for content type.
func FormatContentType(ct string) string {
switch strings.ToLower(ct) {
case "movie":
return "Movie"
case "show":
return "Show"
default:
return ct
}
}
// Ptr returns a pointer to a value. Useful for tests.
func Ptr[T any](v T) *T {
return &v
}
// TruncateString truncates a string to maxLen with ellipsis.
func TruncateString(s string, maxLen int) string {
runes := []rune(s)
if len(runes) <= maxLen {
return s
}
if maxLen <= 3 {
return string(runes[:maxLen])
}
return string(runes[:maxLen-3]) + "..."
}
// FormatLanguages joins language codes.
func FormatLanguages(langs []string) string {
if len(langs) == 0 {
return "-"
}
return strings.Join(langs, ", ")
}
// FormatSeedRatio returns a display for seed/leech ratio.
func FormatSeedRatio(seeders, leechers int) string {
if leechers == 0 {
if seeders == 0 {
return "0:0"
}
return fmt.Sprintf("%d:0", seeders)
}
ratio := float64(seeders) / float64(leechers)
return fmt.Sprintf("%.0f:1", math.Round(ratio))
}
// FormatTimeAgo returns a human-readable "time ago" string.
func FormatTimeAgo(t string) string {
parsed, err := time.Parse(time.RFC3339, t)
if err != nil {
return t
}
diff := time.Since(parsed)
switch {
case diff < time.Minute:
return "just now"
case diff < time.Hour:
m := int(diff.Minutes())
return fmt.Sprintf("%dm ago", m)
case diff < 24*time.Hour:
h := int(diff.Hours())
return fmt.Sprintf("%dh ago", h)
case diff < 30*24*time.Hour:
d := int(diff.Hours() / 24)
return fmt.Sprintf("%dd ago", d)
default:
m := int(diff.Hours() / 24 / 30)
return fmt.Sprintf("%dmo ago", m)
}
}
// FormatNumber formats a number with thousands separator.
func FormatNumber(n int) string {
negative := n < 0
if negative {
n = -n
}
s := strconv.Itoa(n)
if len(s) <= 3 {
if negative {
return "-" + s
}
return s
}
var result []byte
for i, c := range s {
if i > 0 && (len(s)-i)%3 == 0 {
result = append(result, ',')
}
result = append(result, byte(c))
}
if negative {
return "-" + string(result)
}
return string(result)
}
// StringOrDash returns the string value or "-" if nil.
func StringOrDash(s *string) string {
if s == nil {
return "-"
}
return *s
}