mirror of
https://github.com/mudler/luet.git
synced 2025-07-31 23:05:03 +00:00
409 lines
10 KiB
Go
409 lines
10 KiB
Go
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
// Package spinner is a simple package to add a spinner / progress indicator to any terminal application.
|
|
package spinner
|
|
|
|
import (
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"runtime"
|
|
"strconv"
|
|
"sync"
|
|
"time"
|
|
"unicode/utf8"
|
|
|
|
"github.com/fatih/color"
|
|
)
|
|
|
|
// errInvalidColor is returned when attempting to set an invalid color
|
|
var errInvalidColor = errors.New("invalid color")
|
|
|
|
// validColors holds an array of the only colors allowed
|
|
var validColors = map[string]bool{
|
|
// default colors for backwards compatibility
|
|
"black": true,
|
|
"red": true,
|
|
"green": true,
|
|
"yellow": true,
|
|
"blue": true,
|
|
"magenta": true,
|
|
"cyan": true,
|
|
"white": true,
|
|
|
|
// attributes
|
|
"reset": true,
|
|
"bold": true,
|
|
"faint": true,
|
|
"italic": true,
|
|
"underline": true,
|
|
"blinkslow": true,
|
|
"blinkrapid": true,
|
|
"reversevideo": true,
|
|
"concealed": true,
|
|
"crossedout": true,
|
|
|
|
// foreground text
|
|
"fgBlack": true,
|
|
"fgRed": true,
|
|
"fgGreen": true,
|
|
"fgYellow": true,
|
|
"fgBlue": true,
|
|
"fgMagenta": true,
|
|
"fgCyan": true,
|
|
"fgWhite": true,
|
|
|
|
// foreground Hi-Intensity text
|
|
"fgHiBlack": true,
|
|
"fgHiRed": true,
|
|
"fgHiGreen": true,
|
|
"fgHiYellow": true,
|
|
"fgHiBlue": true,
|
|
"fgHiMagenta": true,
|
|
"fgHiCyan": true,
|
|
"fgHiWhite": true,
|
|
|
|
// background text
|
|
"bgBlack": true,
|
|
"bgRed": true,
|
|
"bgGreen": true,
|
|
"bgYellow": true,
|
|
"bgBlue": true,
|
|
"bgMagenta": true,
|
|
"bgCyan": true,
|
|
"bgWhite": true,
|
|
|
|
// background Hi-Intensity text
|
|
"bgHiBlack": true,
|
|
"bgHiRed": true,
|
|
"bgHiGreen": true,
|
|
"bgHiYellow": true,
|
|
"bgHiBlue": true,
|
|
"bgHiMagenta": true,
|
|
"bgHiCyan": true,
|
|
"bgHiWhite": true,
|
|
}
|
|
|
|
// returns a valid color's foreground text color attribute
|
|
var colorAttributeMap = map[string]color.Attribute{
|
|
// default colors for backwards compatibility
|
|
"black": color.FgBlack,
|
|
"red": color.FgRed,
|
|
"green": color.FgGreen,
|
|
"yellow": color.FgYellow,
|
|
"blue": color.FgBlue,
|
|
"magenta": color.FgMagenta,
|
|
"cyan": color.FgCyan,
|
|
"white": color.FgWhite,
|
|
|
|
// attributes
|
|
"reset": color.Reset,
|
|
"bold": color.Bold,
|
|
"faint": color.Faint,
|
|
"italic": color.Italic,
|
|
"underline": color.Underline,
|
|
"blinkslow": color.BlinkSlow,
|
|
"blinkrapid": color.BlinkRapid,
|
|
"reversevideo": color.ReverseVideo,
|
|
"concealed": color.Concealed,
|
|
"crossedout": color.CrossedOut,
|
|
|
|
// foreground text colors
|
|
"fgBlack": color.FgBlack,
|
|
"fgRed": color.FgRed,
|
|
"fgGreen": color.FgGreen,
|
|
"fgYellow": color.FgYellow,
|
|
"fgBlue": color.FgBlue,
|
|
"fgMagenta": color.FgMagenta,
|
|
"fgCyan": color.FgCyan,
|
|
"fgWhite": color.FgWhite,
|
|
|
|
// foreground Hi-Intensity text colors
|
|
"fgHiBlack": color.FgHiBlack,
|
|
"fgHiRed": color.FgHiRed,
|
|
"fgHiGreen": color.FgHiGreen,
|
|
"fgHiYellow": color.FgHiYellow,
|
|
"fgHiBlue": color.FgHiBlue,
|
|
"fgHiMagenta": color.FgHiMagenta,
|
|
"fgHiCyan": color.FgHiCyan,
|
|
"fgHiWhite": color.FgHiWhite,
|
|
|
|
// background text colors
|
|
"bgBlack": color.BgBlack,
|
|
"bgRed": color.BgRed,
|
|
"bgGreen": color.BgGreen,
|
|
"bgYellow": color.BgYellow,
|
|
"bgBlue": color.BgBlue,
|
|
"bgMagenta": color.BgMagenta,
|
|
"bgCyan": color.BgCyan,
|
|
"bgWhite": color.BgWhite,
|
|
|
|
// background Hi-Intensity text colors
|
|
"bgHiBlack": color.BgHiBlack,
|
|
"bgHiRed": color.BgHiRed,
|
|
"bgHiGreen": color.BgHiGreen,
|
|
"bgHiYellow": color.BgHiYellow,
|
|
"bgHiBlue": color.BgHiBlue,
|
|
"bgHiMagenta": color.BgHiMagenta,
|
|
"bgHiCyan": color.BgHiCyan,
|
|
"bgHiWhite": color.BgHiWhite,
|
|
}
|
|
|
|
// validColor will make sure the given color is actually allowed
|
|
func validColor(c string) bool {
|
|
if validColors[c] {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Spinner struct to hold the provided options
|
|
type Spinner struct {
|
|
Delay time.Duration // Delay is the speed of the indicator
|
|
chars []string // chars holds the chosen character set
|
|
Prefix string // Prefix is the text preppended to the indicator
|
|
Suffix string // Suffix is the text appended to the indicator
|
|
FinalMSG string // string displayed after Stop() is called
|
|
lastOutput string // last character(set) written
|
|
color func(a ...interface{}) string // default color is white
|
|
lock *sync.RWMutex //
|
|
Writer io.Writer // to make testing better, exported so users have access
|
|
active bool // active holds the state of the spinner
|
|
stopChan chan struct{} // stopChan is a channel used to stop the indicator
|
|
HideCursor bool // hideCursor determines if the cursor is visible
|
|
}
|
|
|
|
// New provides a pointer to an instance of Spinner with the supplied options
|
|
func New(cs []string, d time.Duration, options ...Option) *Spinner {
|
|
s := &Spinner{
|
|
Delay: d,
|
|
chars: cs,
|
|
color: color.New(color.FgWhite).SprintFunc(),
|
|
lock: &sync.RWMutex{},
|
|
Writer: color.Output,
|
|
active: false,
|
|
stopChan: make(chan struct{}, 1),
|
|
}
|
|
|
|
for _, option := range options {
|
|
option(s)
|
|
}
|
|
return s
|
|
}
|
|
|
|
// Option is a function that takes a spinner and applies
|
|
// a given configuration
|
|
type Option func(*Spinner)
|
|
|
|
// Options contains fields to configure the spinner
|
|
type Options struct {
|
|
Color string
|
|
Suffix string
|
|
FinalMSG string
|
|
HideCursor bool
|
|
}
|
|
|
|
// WithColor adds the given color to the spinner
|
|
func WithColor(color string) Option {
|
|
return func(s *Spinner) {
|
|
s.Color(color)
|
|
}
|
|
}
|
|
|
|
// WithSuffix adds the given string to the spinner
|
|
// as the suffix
|
|
func WithSuffix(suffix string) Option {
|
|
return func(s *Spinner) {
|
|
s.Suffix = suffix
|
|
}
|
|
}
|
|
|
|
// WithFinalMSG adds the given string ot the spinner
|
|
// as the final message to be written
|
|
func WithFinalMSG(finalMsg string) Option {
|
|
return func(s *Spinner) {
|
|
s.FinalMSG = finalMsg
|
|
}
|
|
}
|
|
|
|
// WithHiddenCursor hides the cursor
|
|
// if hideCursor = true given
|
|
func WithHiddenCursor(hideCursor bool) Option {
|
|
return func(s *Spinner) {
|
|
s.HideCursor = hideCursor
|
|
}
|
|
}
|
|
|
|
// Active will return whether or not the spinner is currently active
|
|
func (s *Spinner) Active() bool {
|
|
return s.active
|
|
}
|
|
|
|
// Start will start the indicator
|
|
func (s *Spinner) Start() {
|
|
s.lock.Lock()
|
|
if s.active {
|
|
s.lock.Unlock()
|
|
return
|
|
}
|
|
if s.HideCursor && runtime.GOOS != "windows" {
|
|
// hides the cursor
|
|
fmt.Print("\033[?25l")
|
|
}
|
|
s.active = true
|
|
s.lock.Unlock()
|
|
|
|
go func() {
|
|
for {
|
|
for i := 0; i < len(s.chars); i++ {
|
|
select {
|
|
case <-s.stopChan:
|
|
return
|
|
default:
|
|
s.lock.Lock()
|
|
s.erase()
|
|
var outColor string
|
|
if runtime.GOOS == "windows" {
|
|
if s.Writer == os.Stderr {
|
|
outColor = fmt.Sprintf("\r%s%s%s ", s.Prefix, s.chars[i], s.Suffix)
|
|
} else {
|
|
outColor = fmt.Sprintf("\r%s%s%s ", s.Prefix, s.color(s.chars[i]), s.Suffix)
|
|
}
|
|
} else {
|
|
outColor = fmt.Sprintf("%s%s%s ", s.Prefix, s.color(s.chars[i]), s.Suffix)
|
|
}
|
|
outPlain := fmt.Sprintf("%s%s%s ", s.Prefix, s.chars[i], s.Suffix)
|
|
fmt.Fprint(s.Writer, outColor)
|
|
s.lastOutput = outPlain
|
|
delay := s.Delay
|
|
s.lock.Unlock()
|
|
|
|
time.Sleep(delay)
|
|
}
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
// Stop stops the indicator
|
|
func (s *Spinner) Stop() {
|
|
s.lock.Lock()
|
|
defer s.lock.Unlock()
|
|
if s.active {
|
|
s.active = false
|
|
if s.HideCursor && runtime.GOOS != "windows" {
|
|
// makes the cursor visible
|
|
fmt.Print("\033[?25h")
|
|
}
|
|
s.erase()
|
|
if s.FinalMSG != "" {
|
|
fmt.Fprintf(s.Writer, s.FinalMSG)
|
|
}
|
|
s.stopChan <- struct{}{}
|
|
}
|
|
}
|
|
|
|
// Restart will stop and start the indicator
|
|
func (s *Spinner) Restart() {
|
|
s.Stop()
|
|
s.Start()
|
|
}
|
|
|
|
// Reverse will reverse the order of the slice assigned to the indicator
|
|
func (s *Spinner) Reverse() {
|
|
s.lock.Lock()
|
|
defer s.lock.Unlock()
|
|
for i, j := 0, len(s.chars)-1; i < j; i, j = i+1, j-1 {
|
|
s.chars[i], s.chars[j] = s.chars[j], s.chars[i]
|
|
}
|
|
}
|
|
|
|
// Color will set the struct field for the given color to be used
|
|
func (s *Spinner) Color(colors ...string) error {
|
|
colorAttributes := make([]color.Attribute, len(colors))
|
|
|
|
// Verify colours are valid and place the appropriate attribute in the array
|
|
for index, c := range colors {
|
|
if !validColor(c) {
|
|
return errInvalidColor
|
|
}
|
|
colorAttributes[index] = colorAttributeMap[c]
|
|
}
|
|
|
|
s.lock.Lock()
|
|
s.color = color.New(colorAttributes...).SprintFunc()
|
|
s.lock.Unlock()
|
|
s.Restart()
|
|
return nil
|
|
}
|
|
|
|
// UpdateSpeed will set the indicator delay to the given value
|
|
func (s *Spinner) UpdateSpeed(d time.Duration) {
|
|
s.lock.Lock()
|
|
defer s.lock.Unlock()
|
|
s.Delay = d
|
|
}
|
|
|
|
// UpdateCharSet will change the current character set to the given one
|
|
func (s *Spinner) UpdateCharSet(cs []string) {
|
|
s.lock.Lock()
|
|
defer s.lock.Unlock()
|
|
s.chars = cs
|
|
}
|
|
|
|
// erase deletes written characters
|
|
//
|
|
// Caller must already hold s.lock.
|
|
func (s *Spinner) erase() {
|
|
n := utf8.RuneCountInString(s.lastOutput)
|
|
if runtime.GOOS == "windows" {
|
|
var clearString string
|
|
for i := 0; i < n; i++ {
|
|
clearString += " "
|
|
}
|
|
clearString += "\r"
|
|
fmt.Fprintf(s.Writer, clearString)
|
|
s.lastOutput = ""
|
|
return
|
|
}
|
|
del, _ := hex.DecodeString("7f")
|
|
for _, c := range []string{"\b", string(del), "\b", "\033[K"} { // "\033[K" for macOS Terminal
|
|
for i := 0; i < n; i++ {
|
|
fmt.Fprintf(s.Writer, c)
|
|
}
|
|
}
|
|
s.lastOutput = ""
|
|
}
|
|
|
|
// Lock allows for manual control to lock the spinner
|
|
func (s *Spinner) Lock() {
|
|
s.lock.Lock()
|
|
}
|
|
|
|
// Unlock allows for manual control to unlock the spinner
|
|
func (s *Spinner) Unlock() {
|
|
s.lock.Unlock()
|
|
}
|
|
|
|
// GenerateNumberSequence will generate a slice of integers at the
|
|
// provided length and convert them each to a string
|
|
func GenerateNumberSequence(length int) []string {
|
|
numSeq := make([]string, length)
|
|
for i := 0; i < length; i++ {
|
|
numSeq[i] = strconv.Itoa(i)
|
|
}
|
|
return numSeq
|
|
}
|