453 lines
9.8 KiB
Go
453 lines
9.8 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
"syscall"
|
|
"time"
|
|
"unsafe"
|
|
)
|
|
|
|
// color system
|
|
|
|
type colorType string
|
|
|
|
const (
|
|
colorInput colorType = "input"
|
|
colorSuccess colorType = "success"
|
|
colorInfo colorType = "info"
|
|
colorWarning colorType = "warning"
|
|
colorError colorType = "error"
|
|
colorException colorType = "exception"
|
|
)
|
|
|
|
var colorCodes = map[colorType][3]int{
|
|
colorInput: {0, 200, 200},
|
|
colorSuccess: {80, 220, 80},
|
|
colorInfo: {255, 175, 50},
|
|
colorWarning: {255, 220, 60},
|
|
colorError: {255, 90, 90},
|
|
colorException: {220, 120, 255},
|
|
}
|
|
|
|
var isTerminal = func() bool {
|
|
fi, err := os.Stdout.Stat()
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return fi.Mode()&os.ModeCharDevice != 0
|
|
}()
|
|
|
|
func colorText(textType colorType, text string) string {
|
|
if !isTerminal || (cfg != nil && cfg.noColor) {
|
|
return text
|
|
}
|
|
c, ok := colorCodes[textType]
|
|
if !ok {
|
|
return text
|
|
}
|
|
return fmt.Sprintf("\033[38;2;%d;%d;%dm%s\033[0m", c[0], c[1], c[2], text)
|
|
}
|
|
|
|
func colorStart(textType colorType) string {
|
|
if !isTerminal || (cfg != nil && cfg.noColor) {
|
|
return ""
|
|
}
|
|
c, ok := colorCodes[textType]
|
|
if !ok {
|
|
return ""
|
|
}
|
|
return fmt.Sprintf("\033[38;2;%d;%d;%dm", c[0], c[1], c[2])
|
|
}
|
|
|
|
// terminal width
|
|
|
|
func getTerminalWidth() int {
|
|
if !isTerminal {
|
|
return 80
|
|
}
|
|
type winsize struct {
|
|
Row uint16
|
|
Col uint16
|
|
Xpixel uint16
|
|
Ypixel uint16
|
|
}
|
|
ws := winsize{}
|
|
_, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(os.Stdout.Fd()), uintptr(0x5413), uintptr(unsafe.Pointer(&ws)))
|
|
if errno != 0 || ws.Col == 0 {
|
|
return 80
|
|
}
|
|
return int(ws.Col)
|
|
}
|
|
|
|
// output serialization
|
|
|
|
var outputMu sync.Mutex
|
|
|
|
// logging
|
|
|
|
func logMsg(level, format string, args ...interface{}) {
|
|
outputMu.Lock()
|
|
defer outputMu.Unlock()
|
|
|
|
_clearProgressLine()
|
|
|
|
msg := fmt.Sprintf(format, args...)
|
|
ts := time.Now().Format("15:04:05")
|
|
fmt.Fprintf(os.Stdout, "%s %s %s\n", ts, colorText(colorType(strings.ToLower(level)), fmt.Sprintf("%-7s", level)), msg)
|
|
}
|
|
|
|
func infof(format string, args ...interface{}) { logMsg("INFO", format, args...) }
|
|
func errorf(format string, args ...interface{}) { logMsg("ERROR", format, args...) }
|
|
func warningf(format string, args ...interface{}) {
|
|
logMsg("WARNING", format, args...)
|
|
}
|
|
func debugf(format string, args ...interface{}) {
|
|
if cfg.debug {
|
|
logMsg("DEBUG", format, args...)
|
|
}
|
|
}
|
|
|
|
// status messages
|
|
|
|
func statusMsg(text string) {
|
|
outputMu.Lock()
|
|
defer outputMu.Unlock()
|
|
_clearProgressLine()
|
|
fmt.Printf("%s... ", text)
|
|
fmt.Println(colorText(colorSuccess, "Done"))
|
|
}
|
|
|
|
// download line
|
|
|
|
func downloadLine(counter int, name string, size float64) {
|
|
outputMu.Lock()
|
|
defer outputMu.Unlock()
|
|
_clearProgressLine()
|
|
if size > 0 {
|
|
fmt.Printf("Get:%-3d %s [%s]\n", counter, colorText(colorInfo, name), formatSize(size))
|
|
} else {
|
|
fmt.Printf("Get:%-3d %s\n", counter, colorText(colorInfo, name))
|
|
}
|
|
}
|
|
|
|
// fetch summary
|
|
|
|
func fetchSummary(totalBytes float64, elapsed time.Duration) {
|
|
outputMu.Lock()
|
|
defer outputMu.Unlock()
|
|
_clearProgressLine()
|
|
speed := float64(0)
|
|
if elapsed.Seconds() > 0 {
|
|
speed = totalBytes / elapsed.Seconds()
|
|
}
|
|
fmt.Printf("Fetched %s in %s", formatSize(totalBytes), formatDuration(elapsed))
|
|
if speed > 0 {
|
|
fmt.Printf(" (%s/s)", formatSpeed(speed))
|
|
}
|
|
fmt.Println()
|
|
}
|
|
|
|
func formatSpeed(bytesPerSec float64) string {
|
|
const KB = 1000
|
|
const MB = 1000 * KB
|
|
switch {
|
|
case bytesPerSec >= MB:
|
|
return fmt.Sprintf("%.2f MB/s", bytesPerSec/MB)
|
|
case bytesPerSec >= KB:
|
|
return fmt.Sprintf("%.2f kB/s", bytesPerSec/KB)
|
|
default:
|
|
return fmt.Sprintf("%.0f B/s", bytesPerSec)
|
|
}
|
|
}
|
|
|
|
// size formatting
|
|
|
|
func formatSize(bytes float64) string {
|
|
const (
|
|
KiB = 1000
|
|
MiB = 1000 * KiB
|
|
GiB = 1000 * MiB
|
|
)
|
|
switch {
|
|
case bytes >= GiB:
|
|
return fmt.Sprintf("%.2f GiB", bytes/GiB)
|
|
case bytes >= MiB:
|
|
return fmt.Sprintf("%.2f MiB", bytes/MiB)
|
|
case bytes >= KiB:
|
|
return fmt.Sprintf("%.2f KiB", bytes/KiB)
|
|
default:
|
|
return fmt.Sprintf("%.0f B", bytes)
|
|
}
|
|
}
|
|
|
|
func formatDuration(d time.Duration) string {
|
|
if d < time.Minute {
|
|
return fmt.Sprintf("%ds", int(d.Seconds()))
|
|
}
|
|
if d >= time.Hour {
|
|
h := int(d.Hours())
|
|
m := int(d.Minutes()) % 60
|
|
s := int(d.Seconds()) % 60
|
|
if h >= 100 {
|
|
return fmt.Sprintf("%dh%02dm", h, m)
|
|
}
|
|
return fmt.Sprintf("%dh%02dm%02ds", h, m, s)
|
|
}
|
|
return fmt.Sprintf("%dm%02ds", int(d.Minutes()), int(d.Seconds())%60)
|
|
}
|
|
|
|
// progress tracking
|
|
|
|
type progressState struct {
|
|
mu sync.Mutex
|
|
total int
|
|
completed int
|
|
startTime time.Time
|
|
drawn bool
|
|
spinnerIdx int
|
|
stop chan struct{}
|
|
}
|
|
|
|
var prog *progressState
|
|
|
|
var spinnerChars = [4]byte{'|', '/', '-', '\\'}
|
|
|
|
func newProgressState(total int) *progressState {
|
|
return &progressState{
|
|
total: total,
|
|
startTime: time.Now(),
|
|
stop: make(chan struct{}),
|
|
}
|
|
}
|
|
|
|
func (p *progressState) increment() {
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
if p.completed < p.total {
|
|
p.completed++
|
|
}
|
|
}
|
|
|
|
func (p *progressState) startTicker() {
|
|
go func() {
|
|
ticker := time.NewTicker(200 * time.Millisecond)
|
|
defer ticker.Stop()
|
|
for {
|
|
select {
|
|
case <-p.stop:
|
|
return
|
|
case <-ticker.C:
|
|
outputMu.Lock()
|
|
_drawProgressLine()
|
|
outputMu.Unlock()
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
func (p *progressState) stopTicker() {
|
|
close(p.stop)
|
|
}
|
|
|
|
func _clearProgressLine() {
|
|
if prog == nil || !prog.drawn || !isTerminal || cfg.noColor {
|
|
return
|
|
}
|
|
fmt.Print("\r\033[K")
|
|
prog.drawn = false
|
|
}
|
|
|
|
func _drawProgressLine() {
|
|
if prog == nil || !isTerminal || cfg.noColor || prog.total == 0 {
|
|
return
|
|
}
|
|
|
|
prog.mu.Lock()
|
|
completed := prog.completed
|
|
total := prog.total
|
|
startTime := prog.startTime
|
|
prog.mu.Unlock()
|
|
|
|
// Refresh terminal width each frame so a live resize never wraps the line.
|
|
width := getTerminalWidth()
|
|
|
|
prog.spinnerIdx++
|
|
|
|
fmt.Print("\r\033[K" + buildProgressLine(width, completed, total, time.Since(startTime), prog.spinnerIdx))
|
|
prog.drawn = true
|
|
}
|
|
|
|
func buildProgressLine(width, completed, total int, elapsed time.Duration, spinnerIdx int) string {
|
|
percent := 0
|
|
if total > 0 {
|
|
percent = int(float64(completed) / float64(total) * 100)
|
|
}
|
|
|
|
// overall bar: "Progress: [ NN%] [####.....]" gracefully stolen from APT
|
|
prefix := fmt.Sprintf("Progress: [%3d%%] [%c ", percent, spinnerChars[spinnerIdx%len(spinnerChars)])
|
|
|
|
suffix := fmt.Sprintf("] %d/%d", completed, total)
|
|
|
|
if completed > 0 && elapsed.Seconds() > 1 {
|
|
rate := float64(completed) / elapsed.Seconds()
|
|
remaining := float64(total-completed) / rate
|
|
suffix += fmt.Sprintf(" %s", formatDuration(time.Duration(remaining*float64(time.Second))))
|
|
}
|
|
|
|
barW := width - len(prefix) - len(suffix)
|
|
if barW < 1 {
|
|
line := prefix + suffix
|
|
if len(line) > width {
|
|
line = line[:width]
|
|
}
|
|
return line
|
|
}
|
|
|
|
filled := int(float64(barW) * float64(percent) / 100)
|
|
if filled > barW {
|
|
filled = barW
|
|
}
|
|
bar := strings.Repeat("#", filled) + strings.Repeat(".", barW-filled)
|
|
|
|
return prefix + colorText(colorInfo, bar) + suffix
|
|
}
|
|
|
|
func (p *progressState) finish() {
|
|
p.stopTicker()
|
|
outputMu.Lock()
|
|
defer outputMu.Unlock()
|
|
_clearProgressLine()
|
|
fmt.Println()
|
|
}
|
|
|
|
// transaction summary
|
|
|
|
type packageInfo struct {
|
|
Name string
|
|
Edition string
|
|
Arch string
|
|
Action string
|
|
Size float64
|
|
}
|
|
|
|
func (p packageInfo) String() string {
|
|
return fmt.Sprintf("%s-%s.%s", p.Name, p.Edition, p.Arch)
|
|
}
|
|
|
|
func printTransactionSummary(installs, removes []packageInfo, downloadSize, diffBytes float64, numPkgs int) {
|
|
outputMu.Lock()
|
|
defer outputMu.Unlock()
|
|
_clearProgressLine()
|
|
|
|
fmt.Println()
|
|
|
|
// Count upgrades vs new installs
|
|
nUpgrades := 0
|
|
nInstalls := 0
|
|
for _, pkg := range installs {
|
|
if pkg.Edition != "" && pkg.Arch != "" {
|
|
nUpgrades++
|
|
} else {
|
|
nInstalls++
|
|
}
|
|
}
|
|
nRemoves := len(removes)
|
|
|
|
// Packages to download (upgrades)
|
|
if nUpgrades > 0 {
|
|
names := make([]string, nUpgrades)
|
|
for i, pkg := range installs {
|
|
if i >= nUpgrades {
|
|
break
|
|
}
|
|
names[i] = colorText(colorInfo, pkg.String())
|
|
}
|
|
fmt.Printf("The following packages will be upgraded:\n")
|
|
printWrappedNames(names, getTerminalWidth(), " ")
|
|
fmt.Println()
|
|
}
|
|
|
|
// New installs
|
|
if nInstalls > 0 {
|
|
names := make([]string, nInstalls)
|
|
for i := nUpgrades; i < len(installs) && i-nUpgrades < nInstalls; i++ {
|
|
names[i-nUpgrades] = colorText(colorInfo, installs[i].String())
|
|
}
|
|
fmt.Printf("The following NEW packages will be installed:\n")
|
|
printWrappedNames(names, getTerminalWidth(), " ")
|
|
fmt.Println()
|
|
}
|
|
|
|
// Removed packages
|
|
if nRemoves > 0 {
|
|
names := make([]string, nRemoves)
|
|
for i, pkg := range removes {
|
|
names[i] = colorText(colorError, pkg.String())
|
|
}
|
|
fmt.Printf("The following packages will be REMOVED:\n")
|
|
printWrappedNames(names, getTerminalWidth(), " ")
|
|
fmt.Println()
|
|
}
|
|
|
|
// Summary line
|
|
parts := []string{}
|
|
if nUpgrades > 0 {
|
|
parts = append(parts, fmt.Sprintf("%d upgraded", nUpgrades))
|
|
}
|
|
if nInstalls > 0 {
|
|
parts = append(parts, fmt.Sprintf("%d newly installed", nInstalls))
|
|
}
|
|
if nRemoves > 0 {
|
|
parts = append(parts, fmt.Sprintf("%d to remove", nRemoves))
|
|
}
|
|
parts = append(parts, fmt.Sprintf("%d not upgraded", 0))
|
|
fmt.Printf("%s.\n", strings.Join(parts, ", "))
|
|
|
|
// Download and space info
|
|
if downloadSize > 0 {
|
|
fmt.Printf("Need to get %s of archives.\n", formatSize(downloadSize))
|
|
}
|
|
|
|
spacePrefix := ""
|
|
if diffBytes >= 0 {
|
|
spacePrefix = "+"
|
|
}
|
|
fmt.Printf("After this operation, %s%s of additional disk space will be used.\n",
|
|
spacePrefix, formatSize(diffBytes))
|
|
}
|
|
|
|
// printWrappedNames prints package names wrapped to fit within termWidth.
|
|
func printWrappedNames(names []string, termWidth int, indent string) {
|
|
lineLen := len(indent)
|
|
first := true
|
|
for _, name := range names {
|
|
nameLen := len(name)
|
|
if !first && lineLen+1+nameLen > termWidth-2 {
|
|
fmt.Println()
|
|
lineLen = len(indent)
|
|
first = true
|
|
}
|
|
if !first {
|
|
fmt.Print(" ")
|
|
lineLen++
|
|
}
|
|
fmt.Printf("%s%s", indent, name)
|
|
if first {
|
|
lineLen += nameLen
|
|
first = false
|
|
} else {
|
|
lineLen += nameLen
|
|
}
|
|
}
|
|
fmt.Println()
|
|
}
|
|
|
|
func min(a, b int) int {
|
|
if a < b {
|
|
return a
|
|
}
|
|
return b
|
|
}
|