add progress (Printpf) featuring :\n (1) channel & normal arguments\n (2) right padding to avoid overlaps\n

This commit is contained in:
xdrm-brackets 2019-01-29 18:55:42 +01:00
parent 8240584340
commit 9a73ed5c3c
4 changed files with 134 additions and 0 deletions

View File

@ -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)

107
progress.go Normal file
View File

@ -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)
}
}

22
util.go Normal file
View File

@ -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
}