first commit
This commit is contained in:
commit
c1eb7b0e94
8 changed files with 2304 additions and 0 deletions
453
ui.go
Normal file
453
ui.go
Normal file
|
|
@ -0,0 +1,453 @@
|
|||
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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue