379 lines
8.3 KiB
Go
379 lines
8.3 KiB
Go
package ui
|
||
|
||
import (
|
||
"fmt"
|
||
"os"
|
||
"strings"
|
||
"time"
|
||
)
|
||
|
||
// Colors for terminal output
|
||
const (
|
||
Reset = "\033[0m"
|
||
Red = "\033[31m"
|
||
Green = "\033[32m"
|
||
Yellow = "\033[33m"
|
||
Blue = "\033[34m"
|
||
Magenta = "\033[35m"
|
||
Cyan = "\033[36m"
|
||
White = "\033[37m"
|
||
Bold = "\033[1m"
|
||
)
|
||
|
||
// PacmanUI provides pacman-style interface
|
||
type PacmanUI struct {
|
||
quiet bool
|
||
noColor bool
|
||
termWidth int
|
||
}
|
||
|
||
// NewPacmanUI creates new UI instance
|
||
func NewPacmanUI(quiet bool) *PacmanUI {
|
||
return &PacmanUI{
|
||
quiet: quiet,
|
||
termWidth: getTerminalWidth(),
|
||
}
|
||
}
|
||
|
||
// DisableColor disables colored output
|
||
func (p *PacmanUI) DisableColor() {
|
||
p.noColor = true
|
||
}
|
||
|
||
// color returns color code or empty string if disabled
|
||
func (p *PacmanUI) color(c string) string {
|
||
if p.noColor || p.quiet {
|
||
return ""
|
||
}
|
||
return c
|
||
}
|
||
|
||
// PrintOperation prints operation header like pacman
|
||
func (p *PacmanUI) PrintOperation(op, target string) {
|
||
if p.quiet {
|
||
return
|
||
}
|
||
|
||
opColor := p.color(Cyan + Bold)
|
||
targetColor := p.color(Reset)
|
||
|
||
switch op {
|
||
case "resolving":
|
||
opColor = p.color(Yellow + Bold)
|
||
case "downloading":
|
||
opColor = p.color(Cyan + Bold)
|
||
case "building":
|
||
opColor = p.color(Yellow + Bold)
|
||
case "installing":
|
||
opColor = p.color(Green + Bold)
|
||
case "removing":
|
||
opColor = p.color(Red + Bold)
|
||
}
|
||
|
||
fmt.Printf("%s%s%s %s%s%s\n",
|
||
opColor, op, p.color(Reset),
|
||
targetColor, target, p.color(Reset))
|
||
}
|
||
|
||
// PrintProgress prints pacman-style progress line
|
||
func (p *PacmanUI) PrintProgress(current, total int, pkgName, action string) {
|
||
if p.quiet {
|
||
return
|
||
}
|
||
|
||
percent := float64(current) / float64(total) * 100
|
||
|
||
// Build progress bar
|
||
barWidth := 30
|
||
filled := int(float64(barWidth) * percent / 100)
|
||
empty := barWidth - filled
|
||
|
||
bar := strings.Repeat("#", filled) + strings.Repeat("-", empty)
|
||
|
||
color := p.color(Green)
|
||
if percent < 30 {
|
||
color = p.color(Red)
|
||
} else if percent < 70 {
|
||
color = p.color(Yellow)
|
||
}
|
||
|
||
reset := p.color(Reset)
|
||
|
||
// Clear line and print
|
||
fmt.Printf("\r\033[K[%s%s%s] %s%d/%d%s (%s%.0f%%%s) %s %s",
|
||
color, bar, reset,
|
||
p.color(White+Bold), current, total, reset,
|
||
p.color(Cyan), percent, reset,
|
||
pkgName, action)
|
||
|
||
if current == total {
|
||
fmt.Println() // New line on completion
|
||
}
|
||
}
|
||
|
||
// PrintDownloadProgress prints download progress with speed and ETA
|
||
func (p *PacmanUI) PrintDownloadProgress(pkgName string, downloaded, total int64, speed float64) {
|
||
if p.quiet {
|
||
return
|
||
}
|
||
|
||
percent := float64(downloaded) / float64(total) * 100
|
||
|
||
// Format sizes
|
||
downloadedStr := formatSize(downloaded)
|
||
totalStr := formatSize(total)
|
||
speedStr := formatSpeed(speed)
|
||
|
||
// Calculate ETA
|
||
eta := ""
|
||
if speed > 0 {
|
||
remaining := float64(total-downloaded) / speed
|
||
eta = formatDuration(time.Duration(remaining) * time.Second)
|
||
}
|
||
|
||
barWidth := 25
|
||
filled := int(float64(barWidth) * percent / 100)
|
||
empty := barWidth - filled
|
||
bar := strings.Repeat("#", filled) + strings.Repeat("-", empty)
|
||
|
||
color := p.color(Cyan)
|
||
reset := p.color(Reset)
|
||
|
||
fmt.Printf("\r\033[K %s %s[%s%s%s] %s%s/%s%s %s%s/s ETA: %s",
|
||
pkgName,
|
||
color, bar, reset,
|
||
p.color(White), downloadedStr, totalStr, reset,
|
||
p.color(Yellow), speedStr, reset,
|
||
eta)
|
||
|
||
if downloaded >= total {
|
||
fmt.Println()
|
||
}
|
||
}
|
||
|
||
// PrintSuccess prints success message
|
||
func (p *PacmanUI) PrintSuccess(msg string) {
|
||
if p.quiet {
|
||
return
|
||
}
|
||
fmt.Printf("%s✓%s %s\n", p.color(Green+Bold), p.color(Reset), msg)
|
||
}
|
||
|
||
// PrintError prints error message
|
||
func (p *PacmanUI) PrintError(msg string) {
|
||
fmt.Fprintf(os.Stderr, "%s✗%s %s\n", p.color(Red+Bold), p.color(Reset), msg)
|
||
}
|
||
|
||
// PrintWarning prints warning message
|
||
func (p *PacmanUI) PrintWarning(msg string) {
|
||
if p.quiet {
|
||
return
|
||
}
|
||
fmt.Printf("%s⚠%s %s\n", p.color(Yellow+Bold), p.color(Reset), msg)
|
||
}
|
||
|
||
// PrintInfo prints info message
|
||
func (p *PacmanUI) PrintInfo(msg string) {
|
||
if p.quiet {
|
||
return
|
||
}
|
||
fmt.Printf("%sℹ%s %s\n", p.color(Blue), p.color(Reset), msg)
|
||
}
|
||
|
||
// PrintPackageList prints package list like pacman -Q
|
||
func (p *PacmanUI) PrintPackageList(packages []PackageInfo) {
|
||
if p.quiet {
|
||
return
|
||
}
|
||
|
||
maxNameLen := 0
|
||
for _, pkg := range packages {
|
||
if len(pkg.Name) > maxNameLen {
|
||
maxNameLen = len(pkg.Name)
|
||
}
|
||
}
|
||
|
||
for _, pkg := range packages {
|
||
fmt.Printf("%s%s%s %s%s%s\n",
|
||
p.color(Green+Bold), padRight(pkg.Name, maxNameLen+2), p.color(Reset),
|
||
p.color(Cyan), pkg.Version, p.color(Reset))
|
||
}
|
||
}
|
||
|
||
// PrintTransactionSummary prints transaction summary
|
||
func (p *PacmanUI) PrintTransactionSummary(toInstall, toRemove, toUpgrade []string) {
|
||
if p.quiet {
|
||
return
|
||
}
|
||
|
||
fmt.Println()
|
||
fmt.Printf("%s%sTransaction Summary:%s\n", p.color(Bold), p.color(White), p.color(Reset))
|
||
|
||
if len(toInstall) > 0 {
|
||
fmt.Printf("%sInstall:%s %d packages\n", p.color(Green), p.color(Reset), len(toInstall))
|
||
for _, pkg := range toInstall {
|
||
fmt.Printf(" %s+%s %s\n", p.color(Green), p.color(Reset), pkg)
|
||
}
|
||
}
|
||
|
||
if len(toRemove) > 0 {
|
||
fmt.Printf("%sRemove:%s %d packages\n", p.color(Red), p.color(Reset), len(toRemove))
|
||
for _, pkg := range toRemove {
|
||
fmt.Printf(" %s-%s %s\n", p.color(Red), p.color(Reset), pkg)
|
||
}
|
||
}
|
||
|
||
if len(toUpgrade) > 0 {
|
||
fmt.Printf("%sUpgrade:%s %d packages\n", p.color(Yellow), p.color(Reset), len(toUpgrade))
|
||
for _, pkg := range toUpgrade {
|
||
fmt.Printf(" %s*%s %s\n", p.color(Yellow), p.color(Reset), pkg)
|
||
}
|
||
}
|
||
|
||
fmt.Println()
|
||
}
|
||
|
||
// MultiProgress tracks multiple parallel operations
|
||
type MultiProgress struct {
|
||
ui *PacmanUI
|
||
items []ProgressItem
|
||
mu chan struct{} // semaphore for thread-safe updates
|
||
}
|
||
|
||
// ProgressItem represents single progress item
|
||
type ProgressItem struct {
|
||
Name string
|
||
Progress float64
|
||
Status string
|
||
Speed string
|
||
Completed bool
|
||
}
|
||
|
||
// NewMultiProgress creates multi-item progress tracker
|
||
func (p *PacmanUI) NewMultiProgress() *MultiProgress {
|
||
return &MultiProgress{
|
||
ui: p,
|
||
mu: make(chan struct{}, 1),
|
||
items: []ProgressItem{},
|
||
}
|
||
}
|
||
|
||
// AddItem adds progress item
|
||
func (mp *MultiProgress) AddItem(name string) int {
|
||
mp.mu <- struct{}{}
|
||
defer func() { <-mp.mu }()
|
||
|
||
idx := len(mp.items)
|
||
mp.items = append(mp.items, ProgressItem{Name: name})
|
||
return idx
|
||
}
|
||
|
||
// Update updates progress item
|
||
func (mp *MultiProgress) Update(idx int, progress float64, status, speed string) {
|
||
if idx < 0 || idx >= len(mp.items) {
|
||
return
|
||
}
|
||
|
||
mp.mu <- struct{}{}
|
||
defer func() { <-mp.mu }()
|
||
|
||
mp.items[idx].Progress = progress
|
||
mp.items[idx].Status = status
|
||
mp.items[idx].Speed = speed
|
||
mp.items[idx].Completed = progress >= 100
|
||
|
||
mp.redraw()
|
||
}
|
||
|
||
// redraw refreshes display
|
||
func (mp *MultiProgress) redraw() {
|
||
if mp.ui.quiet {
|
||
return
|
||
}
|
||
|
||
// Clear previous lines
|
||
for i := 0; i < len(mp.items)+2; i++ {
|
||
fmt.Printf("\033[A\033[K")
|
||
}
|
||
|
||
// Print header
|
||
fmt.Printf("%s%sProgress:%s\n", mp.ui.color(Bold), mp.ui.color(White), mp.ui.color(Reset))
|
||
|
||
// Print items
|
||
for _, item := range mp.items {
|
||
bar := mp.renderBar(item.Progress, 20)
|
||
|
||
statusColor := mp.ui.color(Yellow)
|
||
if item.Completed {
|
||
statusColor = mp.ui.color(Green)
|
||
} else if item.Progress < 20 {
|
||
statusColor = mp.ui.color(Red)
|
||
}
|
||
|
||
speedStr := ""
|
||
if item.Speed != "" {
|
||
speedStr = fmt.Sprintf(" %s%s%s", mp.ui.color(Cyan), item.Speed, mp.ui.color(Reset))
|
||
}
|
||
|
||
fmt.Printf(" %s %s%s%s%s%s\n",
|
||
bar,
|
||
padRight(item.Name, 25),
|
||
statusColor, item.Status, mp.ui.color(Reset),
|
||
speedStr)
|
||
}
|
||
}
|
||
|
||
// renderBar renders progress bar
|
||
func (mp *MultiProgress) renderBar(percent float64, width int) string {
|
||
filled := int(float64(width) * percent / 100)
|
||
empty := width - filled
|
||
|
||
bar := strings.Repeat("█", filled) + strings.Repeat("░", empty)
|
||
|
||
color := mp.ui.color(Green)
|
||
if percent < 30 {
|
||
color = mp.ui.color(Red)
|
||
} else if percent < 70 {
|
||
color = mp.ui.color(Yellow)
|
||
}
|
||
|
||
return fmt.Sprintf("%s[%s]%s %3.0f%%", color, bar, mp.ui.color(Reset), percent)
|
||
}
|
||
|
||
// PackageInfo for list display
|
||
type PackageInfo struct {
|
||
Name string
|
||
Version string
|
||
Desc string
|
||
}
|
||
|
||
// Helper functions
|
||
|
||
func formatSize(bytes int64) string {
|
||
const unit = 1024
|
||
if bytes < unit {
|
||
return fmt.Sprintf("%d B", bytes)
|
||
}
|
||
div, exp := int64(unit), 0
|
||
for n := bytes / unit; n >= unit; n /= unit {
|
||
div *= unit
|
||
exp++
|
||
}
|
||
return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp])
|
||
}
|
||
|
||
func formatSpeed(bytesPerSec float64) string {
|
||
return formatSize(int64(bytesPerSec))
|
||
}
|
||
|
||
func padRight(s string, length int) string {
|
||
if len(s) >= length {
|
||
return s
|
||
}
|
||
return s + strings.Repeat(" ", length-len(s))
|
||
}
|
||
|
||
func getTerminalWidth() int {
|
||
// Default to 80 if can't determine
|
||
return 80
|
||
}
|