add progress (Printpf) featuring :\n (1) channel & normal arguments\n (2) right padding to avoid overlaps\n
This commit is contained in:
parent
8240584340
commit
9a73ed5c3c
|
@ -12,6 +12,8 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var ErrInvalidFormat = fmt.Errorf("invalid format")
|
||||||
|
|
||||||
var theme = color.DefaultTheme()
|
var theme = color.DefaultTheme()
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -25,6 +27,9 @@ var (
|
||||||
func Sprintf(format string, a ...interface{}) (string, error) {
|
func Sprintf(format string, a ...interface{}) (string, error) {
|
||||||
// 1. Pre-process format with 'fmt'
|
// 1. Pre-process format with 'fmt'
|
||||||
formatted := fmt.Sprintf(format, a...)
|
formatted := fmt.Sprintf(format, a...)
|
||||||
|
if strings.Contains(formatted, "%!") { // error
|
||||||
|
return "", ErrInvalidFormat
|
||||||
|
}
|
||||||
|
|
||||||
// 2. Protect escaped characters with tokens
|
// 2. Protect escaped characters with tokens
|
||||||
formatted = strings.Replace(formatted, "\\$", dollarToken, -1)
|
formatted = strings.Replace(formatted, "\\$", dollarToken, -1)
|
|
@ -0,0 +1,107 @@
|
||||||
|
package clifmt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrNoNewline = fmt.Errorf("no newline allowed in progress mode")
|
||||||
|
|
||||||
|
// Printpf prints a progress (dynamic) line that rewrites itself
|
||||||
|
// on arguments' update
|
||||||
|
func Printpf(format string, args ...interface{}) error {
|
||||||
|
|
||||||
|
// 1. check format
|
||||||
|
if strings.ContainsAny(format, "\n\r") {
|
||||||
|
return ErrNoNewline
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. init
|
||||||
|
fixed := make([]interface{}, len(args), len(args)) // actual values
|
||||||
|
update := make([]chan interface{}, 0, len(args)) // channels that update values
|
||||||
|
updateIndex := make([]int, 0, len(args)) // association [order -> index in @fixed]
|
||||||
|
|
||||||
|
// 3. manage fixed values vs. updatable values (channels)
|
||||||
|
for i, arg := range args {
|
||||||
|
|
||||||
|
// channel -> keep Zero value + store channel
|
||||||
|
if reflect.TypeOf(arg).Kind() == reflect.Chan {
|
||||||
|
updateIndex = append(updateIndex, i)
|
||||||
|
update = append(update, arg.(chan interface{}))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// raw -> set value
|
||||||
|
fixed[i] = arg
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. launch dynamic select for each channel
|
||||||
|
maxlen := 0
|
||||||
|
nselect(update, func(i int, value interface{}, ok bool) {
|
||||||
|
|
||||||
|
// channel is closed -> do nothing
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// extract real index
|
||||||
|
index := updateIndex[i]
|
||||||
|
|
||||||
|
// update value
|
||||||
|
fixed[index] = value
|
||||||
|
|
||||||
|
// ignore on errors (updatable values still NIL)
|
||||||
|
str, err := Sprintf(format, fixed...)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
reallen := displaySize(str)
|
||||||
|
|
||||||
|
// print string
|
||||||
|
fmt.Printf("\r%s", str)
|
||||||
|
|
||||||
|
// pad right to end of max size
|
||||||
|
if reallen < maxlen {
|
||||||
|
pad := make([]byte, 0, maxlen-reallen)
|
||||||
|
for i := reallen; i < maxlen; i++ {
|
||||||
|
pad = append(pad, ' ')
|
||||||
|
}
|
||||||
|
fmt.Printf("%s", pad)
|
||||||
|
} else {
|
||||||
|
maxlen = reallen
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
fmt.Printf("\n")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// nselect selects on N channels
|
||||||
|
func nselect(channels []chan interface{}, handler func(int, interface{}, bool)) {
|
||||||
|
|
||||||
|
// 1. build the case list containing each channel
|
||||||
|
cases := make([]reflect.SelectCase, len(channels))
|
||||||
|
for i, ch := range channels {
|
||||||
|
cases[i] = reflect.SelectCase{Dir: reflect.SelectRecv, Chan: reflect.ValueOf(ch)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. wait for selections
|
||||||
|
remaining := len(cases)
|
||||||
|
for remaining > 0 {
|
||||||
|
index, value, ok := reflect.Select(cases)
|
||||||
|
|
||||||
|
// (1) Closed
|
||||||
|
if !ok {
|
||||||
|
cases[index].Chan = reflect.ValueOf(nil)
|
||||||
|
remaining--
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// (2) Received data
|
||||||
|
handler(index, value, ok)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
package clifmt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
)
|
||||||
|
|
||||||
|
var esc = regexp.MustCompile(`(?m)\[(?:\d+;)*\d+m`)
|
||||||
|
|
||||||
|
// displaySize returns the real size escaping special characters
|
||||||
|
func displaySize(s string) int {
|
||||||
|
|
||||||
|
// 1. get actual size
|
||||||
|
size := len(s)
|
||||||
|
|
||||||
|
// 2. get all terminal coloring matches
|
||||||
|
matches := esc.FindAllString(s, -1)
|
||||||
|
for _, m := range matches {
|
||||||
|
size -= len(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
return size
|
||||||
|
}
|
Loading…
Reference in New Issue