clifmt/progress.go

103 lines
2.4 KiB
Go

package clifmt
import (
"fmt"
"math"
"reflect"
"strings"
)
// Printpf prints with possible updatable values.
// Arguments can be interface{} (standard fmt.Printf)
// or can be channels (chan interface{}) that will make the
// output update on channel reception.
//
// You SHOULD launch it in a goroutine i.e. go clifmt.Printpf()
// in order for the select{} to work
//
// It can work with multiple lines thanks to ANSI escape sequences
// that allows to rewrite previously written lines
func Printpf(format string, args ...interface{}) error {
// 1. init
values := make([]interface{}, len(args), len(args)) // actual values
channels := make([]chan interface{}, 0, len(args)) // channels that update values
indexes := make([]int, 0, len(args)) // association [channel order -> index in @fixed]
// 2. manage values vs. channels
for i, arg := range args {
// channel -> keep Zero value + store channel
if reflect.TypeOf(arg).Kind() == reflect.Chan {
indexes = append(indexes, i)
channels = append(channels, arg.(chan interface{}))
continue
}
// raw -> set value
values[i] = arg
}
// 3. launch dynamic select for each channel
var rows int = -1
nselect(channels, func(i int, value interface{}, ok bool) {
// (1) channel is closed -> do nothing
if !ok {
return
}
// (2) update value
index := indexes[i]
values[index] = value
// (3) don't print on error (values still NIL)
str, err := Sprintf(format, values...)
if err != nil {
return
}
// (4) rewind N lines (written previous time)
if rows >= 0 {
fmt.Printf("\x1b[%dF\x1b[K", rows)
}
rows = int(math.Max(float64(strings.Count(str, "\n")), 1))
// (5) make each line rewrite previous line
str = strings.Replace(str, "\n", "\n\x1b[K", 11)
// (6) print string
fmt.Printf("%s", str)
})
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)
}
}