зеркало из https://github.com/mozilla/mig.git
[minor] show a pretty progress bar when follow actions in clients
This commit is contained in:
Родитель
bde41c6d4a
Коммит
8541f1a2d6
1
Makefile
1
Makefile
|
@ -121,6 +121,7 @@ go_vendor_dependencies:
|
|||
$(GOGETTER) golang.org/x/net/ipv4
|
||||
$(GOGETTER) golang.org/x/net/ipv6
|
||||
$(GOGETTER) gopkg.in/gcfg.v1
|
||||
$(GOGETTER) github.com/cheggaaa/pb
|
||||
echo 'removing .git from vendored pkg and moving them to vendor'
|
||||
find .tmpdeps/src -type d -name ".git" ! -name ".gitignore" -exec rm -rf {} \; || exit 0
|
||||
cp -ar .tmpdeps/src/* vendor/
|
||||
|
|
|
@ -23,6 +23,7 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cheggaaa/pb"
|
||||
"github.com/jvehent/cljs"
|
||||
"golang.org/x/crypto/openpgp"
|
||||
"gopkg.in/gcfg.v1"
|
||||
|
@ -722,24 +723,27 @@ func (cli Client) EvaluateAgentTarget(target string) (agents []mig.Agent, err er
|
|||
|
||||
// FollowAction continuously loops over an action and prints its completion status in os.Stderr.
|
||||
// when the action reaches its expiration date, FollowAction prints its final status and returns.
|
||||
func (cli Client) FollowAction(a mig.Action) (err error) {
|
||||
func (cli Client) FollowAction(a mig.Action, total int) (err error) {
|
||||
defer func() {
|
||||
if e := recover(); e != nil {
|
||||
err = fmt.Errorf("followAction() -> %v", e)
|
||||
}
|
||||
}()
|
||||
fmt.Fprintf(os.Stderr, "\x1b[34mFollowing action ID %.0f.\x1b[0m", a.ID)
|
||||
sent := 0
|
||||
dotter := 0
|
||||
fmt.Fprintf(os.Stderr, "\x1b[34mFollowing action ID %.0f.\x1b[0m\n", a.ID)
|
||||
previousctr := 0
|
||||
status := ""
|
||||
attempts := 0
|
||||
var completion float64
|
||||
bar := pb.New(total)
|
||||
bar.ShowSpeed = true
|
||||
bar.SetMaxWidth(80)
|
||||
bar.Output = os.Stderr
|
||||
bar.Start()
|
||||
for {
|
||||
a, _, err = cli.GetAction(a.ID)
|
||||
if err != nil {
|
||||
attempts++
|
||||
time.Sleep(1 * time.Second)
|
||||
time.Sleep(time.Second)
|
||||
if attempts >= 30 {
|
||||
panic("failed to retrieve action after 30 seconds. launch may have failed")
|
||||
}
|
||||
|
@ -748,10 +752,6 @@ func (cli Client) FollowAction(a mig.Action) (err error) {
|
|||
if status == "" {
|
||||
status = a.Status
|
||||
}
|
||||
if status != a.Status {
|
||||
fmt.Fprintf(os.Stderr, "\x1b[34mstatus=%s\x1b[0m", a.Status)
|
||||
status = a.Status
|
||||
}
|
||||
// exit follower mode if status isn't one we follow,
|
||||
// or enough commands have returned
|
||||
// or expiration time has passed
|
||||
|
@ -761,33 +761,26 @@ func (cli Client) FollowAction(a mig.Action) (err error) {
|
|||
goto finish
|
||||
break
|
||||
}
|
||||
// init counters
|
||||
if sent == 0 {
|
||||
if a.Counters.Sent == 0 {
|
||||
time.Sleep(1 * time.Second)
|
||||
continue
|
||||
} else {
|
||||
sent = a.Counters.Sent
|
||||
}
|
||||
}
|
||||
if a.Counters.Done > 0 && a.Counters.Done > previousctr {
|
||||
completion = (float64(a.Counters.Done) / float64(a.Counters.Sent)) * 100
|
||||
if completion < 99.5 {
|
||||
bar.Add(a.Counters.Done - previousctr)
|
||||
bar.Update()
|
||||
previousctr = a.Counters.Done
|
||||
fmt.Fprintf(os.Stderr, "\x1b[34m%.0f%%\x1b[0m", completion)
|
||||
}
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "\x1b[34m.\x1b[0m")
|
||||
time.Sleep(2 * time.Second)
|
||||
dotter++
|
||||
}
|
||||
finish:
|
||||
bar.Add(total - previousctr)
|
||||
bar.Update()
|
||||
bar.Finish()
|
||||
a, _, err = cli.GetAction(a.ID)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "[error] failed to retrieve action counters\n")
|
||||
} else {
|
||||
completion = (float64(a.Counters.Done) / float64(a.Counters.Sent)) * 100
|
||||
fmt.Fprintf(os.Stderr, "\n\x1b[34m- %2.1f%% done in %s\x1b[0m\n", completion, time.Now().Sub(a.StartTime).String())
|
||||
fmt.Fprintf(os.Stderr, "\x1b[34m%2.1f%% done in %s\x1b[0m\n", completion, time.Now().Sub(a.StartTime).String())
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "\x1b[34m")
|
||||
a.PrintCounters()
|
||||
|
|
|
@ -8,14 +8,15 @@ package main
|
|||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/bobappleyard/readline"
|
||||
"io"
|
||||
"mig.ninja/mig"
|
||||
"mig.ninja/mig/client"
|
||||
"mig.ninja/mig/modules"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/bobappleyard/readline"
|
||||
"mig.ninja/mig"
|
||||
"mig.ninja/mig/client"
|
||||
"mig.ninja/mig/modules"
|
||||
)
|
||||
|
||||
// default expiration is 300 seconds
|
||||
|
@ -29,7 +30,10 @@ func actionLauncher(tpl mig.Action, cli client.Client) (err error) {
|
|||
err = fmt.Errorf("actionLauncher() -> %v", e)
|
||||
}
|
||||
}()
|
||||
var a mig.Action
|
||||
var (
|
||||
a mig.Action
|
||||
tcount int
|
||||
)
|
||||
if tpl.ID == 0 {
|
||||
fmt.Println("Entering action launcher with empty template")
|
||||
} else {
|
||||
|
@ -188,12 +192,12 @@ times show the various timestamps of the action
|
|||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
count := len(agents)
|
||||
if count == 0 {
|
||||
tcount = len(agents)
|
||||
if tcount == 0 {
|
||||
fmt.Println("0 agents match this target. launch aborted")
|
||||
break
|
||||
}
|
||||
fmt.Printf("%d agents will be targeted by search \"%s\"\n", count, a.Target)
|
||||
fmt.Printf("%d agents will be targeted by search \"%s\"\n", tcount, a.Target)
|
||||
input, err = readline.String("continue? (y/n)> ")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
|
@ -230,7 +234,7 @@ times show the various timestamps of the action
|
|||
fmt.Printf("Action '%s' successfully launched with ID '%.0f' on target '%s'\n",
|
||||
a.Name, a.ID, a.Target)
|
||||
if follow {
|
||||
err = cli.FollowAction(a)
|
||||
err = cli.FollowAction(a, tcount)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
@ -286,7 +290,8 @@ times show the various timestamps of the action
|
|||
fmt.Println(err)
|
||||
break
|
||||
}
|
||||
fmt.Printf("%d agents will be targetted. To get the list, use 'listagents'\n", len(agents))
|
||||
tcount = len(agents)
|
||||
fmt.Printf("%d agents will be targetted. To get the list, use 'listagents'\n", tcount)
|
||||
hasEvaluatedTarget = true
|
||||
case "settimes":
|
||||
// set the dates
|
||||
|
|
|
@ -279,7 +279,7 @@ readytolaunch:
|
|||
done := make(chan bool, 1)
|
||||
signal.Notify(c, os.Interrupt)
|
||||
go func() {
|
||||
err = cli.FollowAction(a)
|
||||
err = cli.FollowAction(a, len(agents))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
language: go
|
||||
go:
|
||||
- 1.4.2
|
||||
sudo: false
|
|
@ -0,0 +1,12 @@
|
|||
Copyright (c) 2012-2015, Sergey Cherepanov
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
||||
|
||||
* Neither the name of the author nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@ -0,0 +1,173 @@
|
|||
# Terminal progress bar for Go
|
||||
|
||||
Simple progress bar for console programs.
|
||||
|
||||
|
||||
## Installation
|
||||
|
||||
```
|
||||
go get github.com/cheggaaa/pb
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```Go
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/cheggaaa/pb"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
count := 100000
|
||||
bar := pb.StartNew(count)
|
||||
for i := 0; i < count; i++ {
|
||||
bar.Increment()
|
||||
time.Sleep(time.Millisecond)
|
||||
}
|
||||
bar.FinishPrint("The End!")
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
Result will be like this:
|
||||
|
||||
```
|
||||
> go run test.go
|
||||
37158 / 100000 [================>_______________________________] 37.16% 1m11s
|
||||
```
|
||||
|
||||
## Customization
|
||||
|
||||
```Go
|
||||
// create bar
|
||||
bar := pb.New(count)
|
||||
|
||||
// refresh info every second (default 200ms)
|
||||
bar.SetRefreshRate(time.Second)
|
||||
|
||||
// show percents (by default already true)
|
||||
bar.ShowPercent = true
|
||||
|
||||
// show bar (by default already true)
|
||||
bar.ShowBar = true
|
||||
|
||||
// no counters
|
||||
bar.ShowCounters = false
|
||||
|
||||
// show "time left"
|
||||
bar.ShowTimeLeft = true
|
||||
|
||||
// show average speed
|
||||
bar.ShowSpeed = true
|
||||
|
||||
// sets the width of the progress bar
|
||||
bar.SetWidth(80)
|
||||
|
||||
// sets the width of the progress bar, but if terminal size smaller will be ignored
|
||||
bar.SetMaxWidth(80)
|
||||
|
||||
// convert output to readable format (like KB, MB)
|
||||
bar.SetUnits(pb.U_BYTES)
|
||||
|
||||
// and start
|
||||
bar.Start()
|
||||
```
|
||||
|
||||
## Progress bar for IO Operations
|
||||
|
||||
```go
|
||||
// create and start bar
|
||||
bar := pb.New(myDataLen).SetUnits(pb.U_BYTES)
|
||||
bar.Start()
|
||||
|
||||
// my io.Reader
|
||||
r := myReader
|
||||
|
||||
// my io.Writer
|
||||
w := myWriter
|
||||
|
||||
// create proxy reader
|
||||
reader := bar.NewProxyReader(r)
|
||||
|
||||
// and copy from pb reader
|
||||
io.Copy(w, reader)
|
||||
|
||||
```
|
||||
|
||||
```go
|
||||
// create and start bar
|
||||
bar := pb.New(myDataLen).SetUnits(pb.U_BYTES)
|
||||
bar.Start()
|
||||
|
||||
// my io.Reader
|
||||
r := myReader
|
||||
|
||||
// my io.Writer
|
||||
w := myWriter
|
||||
|
||||
// create multi writer
|
||||
writer := io.MultiWriter(w, bar)
|
||||
|
||||
// and copy
|
||||
io.Copy(writer, r)
|
||||
```
|
||||
|
||||
## Custom Progress Bar Look-and-feel
|
||||
|
||||
```go
|
||||
bar.Format("<.- >")
|
||||
```
|
||||
|
||||
## Multiple Progress Bars (experimental and unstable)
|
||||
|
||||
#####Multiple bars do not works at Windows!!!
|
||||
|
||||
Do not print to terminal while pool is active.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"github.com/cheggaaa/pb"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// create bars
|
||||
first := pb.New(200).Prefix("First ")
|
||||
second := pb.New(200).Prefix("Second ")
|
||||
third := pb.New(200).Prefix("Third ")
|
||||
// start pool
|
||||
pool, err := pb.StartPool(first, second, third)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// update bars
|
||||
wg := new(sync.WaitGroup)
|
||||
for _, bar := range []*pb.ProgressBar{first, second, third} {
|
||||
wg.Add(1)
|
||||
go func(cb *pb.ProgressBar) {
|
||||
for n := 0; n < 200; n++ {
|
||||
cb.Increment()
|
||||
time.Sleep(time.Millisecond * time.Duration(rand.Intn(100)))
|
||||
}
|
||||
cb.Finish()
|
||||
wg.Done()
|
||||
}(bar)
|
||||
}
|
||||
wg.Wait()
|
||||
// close pool
|
||||
pool.Stop()
|
||||
```
|
||||
|
||||
The result will be as follows:
|
||||
|
||||
```
|
||||
$ go run example/multiple.go
|
||||
First 141 / 1000 [===============>---------------------------------------] 14.10 % 44s
|
||||
Second 139 / 1000 [==============>---------------------------------------] 13.90 % 44s
|
||||
Third 152 / 1000 [================>--------------------------------------] 15.20 % 40s
|
||||
```
|
|
@ -0,0 +1,82 @@
|
|||
package pb_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cheggaaa/pb"
|
||||
)
|
||||
|
||||
func Example_copy() {
|
||||
// check args
|
||||
if len(os.Args) < 3 {
|
||||
printUsage()
|
||||
return
|
||||
}
|
||||
sourceName, destName := os.Args[1], os.Args[2]
|
||||
|
||||
// check source
|
||||
var source io.Reader
|
||||
var sourceSize int64
|
||||
if strings.HasPrefix(sourceName, "http://") {
|
||||
// open as url
|
||||
resp, err := http.Get(sourceName)
|
||||
if err != nil {
|
||||
fmt.Printf("Can't get %s: %v\n", sourceName, err)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
fmt.Printf("Server return non-200 status: %v\n", resp.Status)
|
||||
return
|
||||
}
|
||||
i, _ := strconv.Atoi(resp.Header.Get("Content-Length"))
|
||||
sourceSize = int64(i)
|
||||
source = resp.Body
|
||||
} else {
|
||||
// open as file
|
||||
s, err := os.Open(sourceName)
|
||||
if err != nil {
|
||||
fmt.Printf("Can't open %s: %v\n", sourceName, err)
|
||||
return
|
||||
}
|
||||
defer s.Close()
|
||||
// get source size
|
||||
sourceStat, err := s.Stat()
|
||||
if err != nil {
|
||||
fmt.Printf("Can't stat %s: %v\n", sourceName, err)
|
||||
return
|
||||
}
|
||||
sourceSize = sourceStat.Size()
|
||||
source = s
|
||||
}
|
||||
|
||||
// create dest
|
||||
dest, err := os.Create(destName)
|
||||
if err != nil {
|
||||
fmt.Printf("Can't create %s: %v\n", destName, err)
|
||||
return
|
||||
}
|
||||
defer dest.Close()
|
||||
|
||||
// create bar
|
||||
bar := pb.New(int(sourceSize)).SetUnits(pb.U_BYTES).SetRefreshRate(time.Millisecond * 10)
|
||||
bar.ShowSpeed = true
|
||||
bar.Start()
|
||||
|
||||
// create proxy reader
|
||||
reader := bar.NewProxyReader(source)
|
||||
|
||||
// and copy from reader
|
||||
io.Copy(dest, reader)
|
||||
bar.Finish()
|
||||
}
|
||||
|
||||
func printUsage() {
|
||||
fmt.Println("copy [source file or url] [dest file]")
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
package pb_test
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/cheggaaa/pb"
|
||||
)
|
||||
|
||||
func Example_multiple() {
|
||||
// create bars
|
||||
first := pb.New(200).Prefix("First ")
|
||||
second := pb.New(200).Prefix("Second ")
|
||||
third := pb.New(200).Prefix("Third ")
|
||||
// start pool
|
||||
pool, err := pb.StartPool(first, second, third)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// update bars
|
||||
wg := new(sync.WaitGroup)
|
||||
for _, bar := range []*pb.ProgressBar{first, second, third} {
|
||||
wg.Add(1)
|
||||
go func(cb *pb.ProgressBar) {
|
||||
for n := 0; n < 200; n++ {
|
||||
cb.Increment()
|
||||
time.Sleep(time.Millisecond * time.Duration(rand.Intn(100)))
|
||||
}
|
||||
cb.Finish()
|
||||
wg.Done()
|
||||
}(bar)
|
||||
}
|
||||
wg.Wait()
|
||||
// close pool
|
||||
pool.Stop()
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
package pb_test
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/cheggaaa/pb"
|
||||
)
|
||||
|
||||
func Example() {
|
||||
count := 5000
|
||||
bar := pb.New(count)
|
||||
|
||||
// show percents (by default already true)
|
||||
bar.ShowPercent = true
|
||||
|
||||
// show bar (by default already true)
|
||||
bar.ShowBar = true
|
||||
|
||||
bar.ShowCounters = true
|
||||
|
||||
bar.ShowTimeLeft = true
|
||||
|
||||
// and start
|
||||
bar.Start()
|
||||
for i := 0; i < count; i++ {
|
||||
bar.Increment()
|
||||
time.Sleep(time.Millisecond)
|
||||
}
|
||||
bar.FinishPrint("The End!")
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
package pb
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Units int
|
||||
|
||||
const (
|
||||
// By default, without type handle
|
||||
U_NO Units = iota
|
||||
// Handle as b, Kb, Mb, etc
|
||||
U_BYTES
|
||||
)
|
||||
|
||||
// Format integer
|
||||
func Format(i int64, units Units) string {
|
||||
switch units {
|
||||
case U_BYTES:
|
||||
return FormatBytes(i)
|
||||
default:
|
||||
// by default just convert to string
|
||||
return strconv.FormatInt(i, 10)
|
||||
}
|
||||
}
|
||||
|
||||
// Convert bytes to human readable string. Like a 2 MB, 64.2 KB, 52 B
|
||||
func FormatBytes(i int64) (result string) {
|
||||
switch {
|
||||
case i > (1024 * 1024 * 1024 * 1024):
|
||||
result = fmt.Sprintf("%.02f TB", float64(i)/1024/1024/1024/1024)
|
||||
case i > (1024 * 1024 * 1024):
|
||||
result = fmt.Sprintf("%.02f GB", float64(i)/1024/1024/1024)
|
||||
case i > (1024 * 1024):
|
||||
result = fmt.Sprintf("%.02f MB", float64(i)/1024/1024)
|
||||
case i > 1024:
|
||||
result = fmt.Sprintf("%.02f KB", float64(i)/1024)
|
||||
default:
|
||||
result = fmt.Sprintf("%d B", i)
|
||||
}
|
||||
result = strings.Trim(result, " ")
|
||||
return
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
package pb
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_DefaultsToInteger(t *testing.T) {
|
||||
value := int64(1000)
|
||||
expected := strconv.Itoa(int(value))
|
||||
actual := Format(value, -1)
|
||||
|
||||
if actual != expected {
|
||||
t.Error(fmt.Sprintf("Expected {%s} was {%s}", expected, actual))
|
||||
}
|
||||
}
|
||||
|
||||
func Test_CanFormatAsInteger(t *testing.T) {
|
||||
value := int64(1000)
|
||||
expected := strconv.Itoa(int(value))
|
||||
actual := Format(value, U_NO)
|
||||
|
||||
if actual != expected {
|
||||
t.Error(fmt.Sprintf("Expected {%s} was {%s}", expected, actual))
|
||||
}
|
||||
}
|
||||
|
||||
func Test_CanFormatAsBytes(t *testing.T) {
|
||||
value := int64(1000)
|
||||
expected := "1000 B"
|
||||
actual := Format(value, U_BYTES)
|
||||
|
||||
if actual != expected {
|
||||
t.Error(fmt.Sprintf("Expected {%s} was {%s}", expected, actual))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,385 @@
|
|||
// Simple console progress bars
|
||||
package pb
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
const (
|
||||
// Default refresh rate - 200ms
|
||||
DEFAULT_REFRESH_RATE = time.Millisecond * 200
|
||||
FORMAT = "[=>-]"
|
||||
)
|
||||
|
||||
// DEPRECATED
|
||||
// variables for backward compatibility, from now do not work
|
||||
// use pb.Format and pb.SetRefreshRate
|
||||
var (
|
||||
DefaultRefreshRate = DEFAULT_REFRESH_RATE
|
||||
BarStart, BarEnd, Empty, Current, CurrentN string
|
||||
)
|
||||
|
||||
// Create new progress bar object
|
||||
func New(total int) *ProgressBar {
|
||||
return New64(int64(total))
|
||||
}
|
||||
|
||||
// Create new progress bar object uding int64 as total
|
||||
func New64(total int64) *ProgressBar {
|
||||
pb := &ProgressBar{
|
||||
Total: total,
|
||||
RefreshRate: DEFAULT_REFRESH_RATE,
|
||||
ShowPercent: true,
|
||||
ShowCounters: true,
|
||||
ShowBar: true,
|
||||
ShowTimeLeft: true,
|
||||
ShowFinalTime: true,
|
||||
Units: U_NO,
|
||||
ManualUpdate: false,
|
||||
finish: make(chan struct{}),
|
||||
currentValue: -1,
|
||||
}
|
||||
return pb.Format(FORMAT)
|
||||
}
|
||||
|
||||
// Create new object and start
|
||||
func StartNew(total int) *ProgressBar {
|
||||
return New(total).Start()
|
||||
}
|
||||
|
||||
// Callback for custom output
|
||||
// For example:
|
||||
// bar.Callback = func(s string) {
|
||||
// mySuperPrint(s)
|
||||
// }
|
||||
//
|
||||
type Callback func(out string)
|
||||
|
||||
type ProgressBar struct {
|
||||
current int64 // current must be first member of struct (https://code.google.com/p/go/issues/detail?id=5278)
|
||||
|
||||
Total int64
|
||||
RefreshRate time.Duration
|
||||
ShowPercent, ShowCounters bool
|
||||
ShowSpeed, ShowTimeLeft, ShowBar bool
|
||||
ShowFinalTime bool
|
||||
Output io.Writer
|
||||
Callback Callback
|
||||
NotPrint bool
|
||||
Units Units
|
||||
Width int
|
||||
ForceWidth bool
|
||||
ManualUpdate bool
|
||||
|
||||
finishOnce sync.Once //Guards isFinish
|
||||
finish chan struct{}
|
||||
isFinish bool
|
||||
|
||||
startTime time.Time
|
||||
startValue int64
|
||||
currentValue int64
|
||||
|
||||
prefix, postfix string
|
||||
|
||||
lastPrint string
|
||||
|
||||
BarStart string
|
||||
BarEnd string
|
||||
Empty string
|
||||
Current string
|
||||
CurrentN string
|
||||
|
||||
AlwaysUpdate bool
|
||||
}
|
||||
|
||||
// Start print
|
||||
func (pb *ProgressBar) Start() *ProgressBar {
|
||||
pb.startTime = time.Now()
|
||||
pb.startValue = pb.current
|
||||
if pb.Total == 0 {
|
||||
pb.ShowTimeLeft = false
|
||||
pb.ShowPercent = false
|
||||
}
|
||||
if !pb.ManualUpdate {
|
||||
go pb.writer()
|
||||
}
|
||||
return pb
|
||||
}
|
||||
|
||||
// Increment current value
|
||||
func (pb *ProgressBar) Increment() int {
|
||||
return pb.Add(1)
|
||||
}
|
||||
|
||||
// Set current value
|
||||
func (pb *ProgressBar) Set(current int) *ProgressBar {
|
||||
return pb.Set64(int64(current))
|
||||
}
|
||||
|
||||
// Set64 sets the current value as int64
|
||||
func (pb *ProgressBar) Set64(current int64) *ProgressBar {
|
||||
atomic.StoreInt64(&pb.current, current)
|
||||
return pb
|
||||
}
|
||||
|
||||
// Add to current value
|
||||
func (pb *ProgressBar) Add(add int) int {
|
||||
return int(pb.Add64(int64(add)))
|
||||
}
|
||||
|
||||
func (pb *ProgressBar) Add64(add int64) int64 {
|
||||
return atomic.AddInt64(&pb.current, add)
|
||||
}
|
||||
|
||||
// Set prefix string
|
||||
func (pb *ProgressBar) Prefix(prefix string) *ProgressBar {
|
||||
pb.prefix = prefix
|
||||
return pb
|
||||
}
|
||||
|
||||
// Set postfix string
|
||||
func (pb *ProgressBar) Postfix(postfix string) *ProgressBar {
|
||||
pb.postfix = postfix
|
||||
return pb
|
||||
}
|
||||
|
||||
// Set custom format for bar
|
||||
// Example: bar.Format("[=>_]")
|
||||
// Example: bar.Format("[\x00=\x00>\x00-\x00]") // \x00 is the delimiter
|
||||
func (pb *ProgressBar) Format(format string) *ProgressBar {
|
||||
var formatEntries []string
|
||||
if len(format) == 5 {
|
||||
formatEntries = strings.Split(format, "")
|
||||
} else {
|
||||
formatEntries = strings.Split(format, "\x00")
|
||||
}
|
||||
if len(formatEntries) == 5 {
|
||||
pb.BarStart = formatEntries[0]
|
||||
pb.BarEnd = formatEntries[4]
|
||||
pb.Empty = formatEntries[3]
|
||||
pb.Current = formatEntries[1]
|
||||
pb.CurrentN = formatEntries[2]
|
||||
}
|
||||
return pb
|
||||
}
|
||||
|
||||
// Set bar refresh rate
|
||||
func (pb *ProgressBar) SetRefreshRate(rate time.Duration) *ProgressBar {
|
||||
pb.RefreshRate = rate
|
||||
return pb
|
||||
}
|
||||
|
||||
// Set units
|
||||
// bar.SetUnits(U_NO) - by default
|
||||
// bar.SetUnits(U_BYTES) - for Mb, Kb, etc
|
||||
func (pb *ProgressBar) SetUnits(units Units) *ProgressBar {
|
||||
pb.Units = units
|
||||
return pb
|
||||
}
|
||||
|
||||
// Set max width, if width is bigger than terminal width, will be ignored
|
||||
func (pb *ProgressBar) SetMaxWidth(width int) *ProgressBar {
|
||||
pb.Width = width
|
||||
pb.ForceWidth = false
|
||||
return pb
|
||||
}
|
||||
|
||||
// Set bar width
|
||||
func (pb *ProgressBar) SetWidth(width int) *ProgressBar {
|
||||
pb.Width = width
|
||||
pb.ForceWidth = true
|
||||
return pb
|
||||
}
|
||||
|
||||
// End print
|
||||
func (pb *ProgressBar) Finish() {
|
||||
//Protect multiple calls
|
||||
pb.finishOnce.Do(func() {
|
||||
close(pb.finish)
|
||||
pb.write(atomic.LoadInt64(&pb.current))
|
||||
if !pb.NotPrint {
|
||||
fmt.Println()
|
||||
}
|
||||
pb.isFinish = true
|
||||
})
|
||||
}
|
||||
|
||||
// End print and write string 'str'
|
||||
func (pb *ProgressBar) FinishPrint(str string) {
|
||||
pb.Finish()
|
||||
fmt.Println(str)
|
||||
}
|
||||
|
||||
// implement io.Writer
|
||||
func (pb *ProgressBar) Write(p []byte) (n int, err error) {
|
||||
n = len(p)
|
||||
pb.Add(n)
|
||||
return
|
||||
}
|
||||
|
||||
// implement io.Reader
|
||||
func (pb *ProgressBar) Read(p []byte) (n int, err error) {
|
||||
n = len(p)
|
||||
pb.Add(n)
|
||||
return
|
||||
}
|
||||
|
||||
// Create new proxy reader over bar
|
||||
func (pb *ProgressBar) NewProxyReader(r io.Reader) *Reader {
|
||||
return &Reader{r, pb}
|
||||
}
|
||||
|
||||
func (pb *ProgressBar) write(current int64) {
|
||||
width := pb.GetWidth()
|
||||
|
||||
var percentBox, countersBox, timeLeftBox, speedBox, barBox, end, out string
|
||||
|
||||
// percents
|
||||
if pb.ShowPercent {
|
||||
percent := float64(current) / (float64(pb.Total) / float64(100))
|
||||
percentBox = fmt.Sprintf(" %.02f %% ", percent)
|
||||
}
|
||||
|
||||
// counters
|
||||
if pb.ShowCounters {
|
||||
if pb.Total > 0 {
|
||||
countersBox = fmt.Sprintf("%s / %s ", Format(current, pb.Units), Format(pb.Total, pb.Units))
|
||||
} else {
|
||||
countersBox = Format(current, pb.Units) + " / ? "
|
||||
}
|
||||
}
|
||||
|
||||
// time left
|
||||
fromStart := time.Now().Sub(pb.startTime)
|
||||
currentFromStart := current - pb.startValue
|
||||
select {
|
||||
case <-pb.finish:
|
||||
if pb.ShowFinalTime {
|
||||
left := (fromStart / time.Second) * time.Second
|
||||
timeLeftBox = left.String()
|
||||
}
|
||||
default:
|
||||
if pb.ShowTimeLeft && currentFromStart > 0 {
|
||||
perEntry := fromStart / time.Duration(currentFromStart)
|
||||
left := time.Duration(pb.Total-currentFromStart) * perEntry
|
||||
left = (left / time.Second) * time.Second
|
||||
timeLeftBox = left.String()
|
||||
}
|
||||
}
|
||||
|
||||
// speed
|
||||
if pb.ShowSpeed && currentFromStart > 0 {
|
||||
fromStart := time.Now().Sub(pb.startTime)
|
||||
speed := float64(currentFromStart) / (float64(fromStart) / float64(time.Second))
|
||||
speedBox = Format(int64(speed), pb.Units) + "/s "
|
||||
}
|
||||
|
||||
barWidth := escapeAwareRuneCountInString(countersBox + pb.BarStart + pb.BarEnd + percentBox + timeLeftBox + speedBox + pb.prefix + pb.postfix)
|
||||
// bar
|
||||
if pb.ShowBar {
|
||||
size := width - barWidth
|
||||
if size > 0 {
|
||||
if pb.Total > 0 {
|
||||
curCount := int(math.Ceil((float64(current) / float64(pb.Total)) * float64(size)))
|
||||
emptCount := size - curCount
|
||||
barBox = pb.BarStart
|
||||
if emptCount < 0 {
|
||||
emptCount = 0
|
||||
}
|
||||
if curCount > size {
|
||||
curCount = size
|
||||
}
|
||||
if emptCount <= 0 {
|
||||
barBox += strings.Repeat(pb.Current, curCount)
|
||||
} else if curCount > 0 {
|
||||
barBox += strings.Repeat(pb.Current, curCount-1) + pb.CurrentN
|
||||
}
|
||||
|
||||
barBox += strings.Repeat(pb.Empty, emptCount) + pb.BarEnd
|
||||
} else {
|
||||
|
||||
barBox = pb.BarStart
|
||||
pos := size - int(current)%int(size)
|
||||
if pos-1 > 0 {
|
||||
barBox += strings.Repeat(pb.Empty, pos-1)
|
||||
}
|
||||
barBox += pb.Current
|
||||
if size-pos-1 > 0 {
|
||||
barBox += strings.Repeat(pb.Empty, size-pos-1)
|
||||
}
|
||||
barBox += pb.BarEnd
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// check len
|
||||
out = pb.prefix + countersBox + barBox + percentBox + speedBox + timeLeftBox + pb.postfix
|
||||
if escapeAwareRuneCountInString(out) < width {
|
||||
end = strings.Repeat(" ", width-utf8.RuneCountInString(out))
|
||||
}
|
||||
|
||||
// and print!
|
||||
pb.lastPrint = out + end
|
||||
switch {
|
||||
case pb.Output != nil:
|
||||
fmt.Fprint(pb.Output, "\r"+out+end)
|
||||
case pb.Callback != nil:
|
||||
pb.Callback(out + end)
|
||||
case !pb.NotPrint:
|
||||
fmt.Print("\r" + out + end)
|
||||
}
|
||||
}
|
||||
|
||||
func (pb *ProgressBar) GetWidth() int {
|
||||
if pb.ForceWidth {
|
||||
return pb.Width
|
||||
}
|
||||
|
||||
width := pb.Width
|
||||
termWidth, _ := terminalWidth()
|
||||
if width == 0 || termWidth <= width {
|
||||
width = termWidth
|
||||
}
|
||||
|
||||
return width
|
||||
}
|
||||
|
||||
// Write the current state of the progressbar
|
||||
func (pb *ProgressBar) Update() {
|
||||
c := atomic.LoadInt64(&pb.current)
|
||||
if pb.AlwaysUpdate || c != pb.currentValue {
|
||||
pb.write(c)
|
||||
pb.currentValue = c
|
||||
}
|
||||
}
|
||||
|
||||
func (pb *ProgressBar) String() string {
|
||||
return pb.lastPrint
|
||||
}
|
||||
|
||||
// Internal loop for writing progressbar
|
||||
func (pb *ProgressBar) writer() {
|
||||
pb.Update()
|
||||
for {
|
||||
select {
|
||||
case <-pb.finish:
|
||||
return
|
||||
case <-time.After(pb.RefreshRate):
|
||||
pb.Update()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type window struct {
|
||||
Row uint16
|
||||
Col uint16
|
||||
Xpixel uint16
|
||||
Ypixel uint16
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
// +build linux darwin freebsd netbsd openbsd dragonfly
|
||||
|
||||
package pb
|
||||
|
||||
import "syscall"
|
||||
|
||||
const sys_ioctl = syscall.SYS_IOCTL
|
|
@ -0,0 +1,5 @@
|
|||
// +build solaris
|
||||
|
||||
package pb
|
||||
|
||||
const sys_ioctl = 54
|
|
@ -0,0 +1,58 @@
|
|||
package pb
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/mattn/go-colorable"
|
||||
)
|
||||
|
||||
func Test_IncrementAddsOne(t *testing.T) {
|
||||
count := 5000
|
||||
bar := New(count)
|
||||
expected := 1
|
||||
actual := bar.Increment()
|
||||
|
||||
if actual != expected {
|
||||
t.Errorf("Expected {%d} was {%d}", expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_Width(t *testing.T) {
|
||||
count := 5000
|
||||
bar := New(count)
|
||||
width := 100
|
||||
bar.SetWidth(100).Callback = func(out string) {
|
||||
if len(out) != width {
|
||||
t.Errorf("Bar width expected {%d} was {%d}", len(out), width)
|
||||
}
|
||||
}
|
||||
bar.Start()
|
||||
bar.Increment()
|
||||
bar.Finish()
|
||||
}
|
||||
|
||||
func Test_MultipleFinish(t *testing.T) {
|
||||
bar := New(5000)
|
||||
bar.Add(2000)
|
||||
bar.Finish()
|
||||
bar.Finish()
|
||||
}
|
||||
|
||||
func Test_Format(t *testing.T) {
|
||||
bar := New(5000).Format(strings.Join([]string{
|
||||
color.GreenString("["),
|
||||
color.New(color.BgGreen).SprintFunc()("o"),
|
||||
color.New(color.BgHiGreen).SprintFunc()("o"),
|
||||
color.New(color.BgRed).SprintFunc()("o"),
|
||||
color.GreenString("]"),
|
||||
}, "\x00"))
|
||||
w := colorable.NewColorableStdout()
|
||||
bar.Callback = func(out string) {
|
||||
w.Write([]byte(out))
|
||||
}
|
||||
bar.Add(2000)
|
||||
bar.Finish()
|
||||
bar.Finish()
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
// +build windows
|
||||
|
||||
package pb
|
||||
|
||||
import (
|
||||
"github.com/olekukonko/ts"
|
||||
"os"
|
||||
)
|
||||
|
||||
var tty = os.Stdin
|
||||
|
||||
func terminalWidth() (int, error) {
|
||||
size, err := ts.GetSize()
|
||||
return size.Col(), err
|
||||
}
|
|
@ -0,0 +1,108 @@
|
|||
// +build linux darwin freebsd netbsd openbsd solaris dragonfly
|
||||
|
||||
package pb
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"runtime"
|
||||
"sync"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
TIOCGWINSZ = 0x5413
|
||||
TIOCGWINSZ_OSX = 1074295912
|
||||
)
|
||||
|
||||
var tty *os.File
|
||||
|
||||
var ErrPoolWasStarted = errors.New("Bar pool was started")
|
||||
|
||||
var echoLocked bool
|
||||
var echoLockMutex sync.Mutex
|
||||
|
||||
func init() {
|
||||
var err error
|
||||
tty, err = os.Open("/dev/tty")
|
||||
if err != nil {
|
||||
tty = os.Stdin
|
||||
}
|
||||
}
|
||||
|
||||
func terminalWidth() (int, error) {
|
||||
w := new(window)
|
||||
tio := syscall.TIOCGWINSZ
|
||||
if runtime.GOOS == "darwin" {
|
||||
tio = TIOCGWINSZ_OSX
|
||||
}
|
||||
res, _, err := syscall.Syscall(sys_ioctl,
|
||||
tty.Fd(),
|
||||
uintptr(tio),
|
||||
uintptr(unsafe.Pointer(w)),
|
||||
)
|
||||
if int(res) == -1 {
|
||||
return 0, err
|
||||
}
|
||||
return int(w.Col), nil
|
||||
}
|
||||
|
||||
var oldState syscall.Termios
|
||||
|
||||
func lockEcho() (quit chan int, err error) {
|
||||
echoLockMutex.Lock()
|
||||
defer echoLockMutex.Unlock()
|
||||
if echoLocked {
|
||||
err = ErrPoolWasStarted
|
||||
return
|
||||
}
|
||||
echoLocked = true
|
||||
|
||||
fd := tty.Fd()
|
||||
if _, _, e := syscall.Syscall6(syscall.SYS_IOCTL, fd, ioctlReadTermios, uintptr(unsafe.Pointer(&oldState)), 0, 0, 0); e != 0 {
|
||||
err = fmt.Errorf("Can't get terminal settings: %v", e)
|
||||
return
|
||||
}
|
||||
|
||||
newState := oldState
|
||||
newState.Lflag &^= syscall.ECHO
|
||||
newState.Lflag |= syscall.ICANON | syscall.ISIG
|
||||
newState.Iflag |= syscall.ICRNL
|
||||
if _, _, e := syscall.Syscall6(syscall.SYS_IOCTL, fd, ioctlWriteTermios, uintptr(unsafe.Pointer(&newState)), 0, 0, 0); e != 0 {
|
||||
err = fmt.Errorf("Can't set terminal settings: %v", e)
|
||||
return
|
||||
}
|
||||
quit = make(chan int, 1)
|
||||
go catchTerminate(quit)
|
||||
return
|
||||
}
|
||||
|
||||
func unlockEcho() (err error) {
|
||||
echoLockMutex.Lock()
|
||||
defer echoLockMutex.Unlock()
|
||||
if !echoLocked {
|
||||
return
|
||||
}
|
||||
echoLocked = false
|
||||
fd := tty.Fd()
|
||||
if _, _, e := syscall.Syscall6(syscall.SYS_IOCTL, fd, ioctlWriteTermios, uintptr(unsafe.Pointer(&oldState)), 0, 0, 0); e != 0 {
|
||||
err = fmt.Errorf("Can't set terminal settings")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// listen exit signals and restore terminal state
|
||||
func catchTerminate(quit chan int) {
|
||||
sig := make(chan os.Signal, 1)
|
||||
signal.Notify(sig, os.Interrupt, syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGKILL)
|
||||
defer signal.Stop(sig)
|
||||
select {
|
||||
case <-quit:
|
||||
unlockEcho()
|
||||
case <-sig:
|
||||
unlockEcho()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
// +build linux darwin freebsd netbsd openbsd solaris dragonfly
|
||||
|
||||
package pb
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Create and start new pool with given bars
|
||||
// You need call pool.Stop() after work
|
||||
func StartPool(pbs ...*ProgressBar) (pool *Pool, err error) {
|
||||
pool = new(Pool)
|
||||
if err = pool.start(); err != nil {
|
||||
return
|
||||
}
|
||||
pool.add(pbs...)
|
||||
return
|
||||
}
|
||||
|
||||
type Pool struct {
|
||||
RefreshRate time.Duration
|
||||
bars []*ProgressBar
|
||||
quit chan int
|
||||
finishOnce sync.Once
|
||||
}
|
||||
|
||||
func (p *Pool) add(pbs ...*ProgressBar) {
|
||||
for _, bar := range pbs {
|
||||
bar.ManualUpdate = true
|
||||
bar.NotPrint = true
|
||||
bar.Start()
|
||||
p.bars = append(p.bars, bar)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Pool) start() (err error) {
|
||||
p.RefreshRate = DefaultRefreshRate
|
||||
quit, err := lockEcho()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
p.quit = make(chan int)
|
||||
go p.writer(quit)
|
||||
return
|
||||
}
|
||||
|
||||
func (p *Pool) writer(finish chan int) {
|
||||
var first = true
|
||||
for {
|
||||
select {
|
||||
case <-time.After(p.RefreshRate):
|
||||
if p.print(first) {
|
||||
p.print(false)
|
||||
finish <- 1
|
||||
return
|
||||
}
|
||||
first = false
|
||||
case <-p.quit:
|
||||
finish <- 1
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Pool) print(first bool) bool {
|
||||
var out string
|
||||
if !first {
|
||||
out = fmt.Sprintf("\033[%dA", len(p.bars))
|
||||
}
|
||||
isFinished := true
|
||||
for _, bar := range p.bars {
|
||||
if !bar.isFinish {
|
||||
isFinished = false
|
||||
}
|
||||
bar.Update()
|
||||
out += fmt.Sprintf("\r%s\n", bar.String())
|
||||
}
|
||||
fmt.Print(out)
|
||||
return isFinished
|
||||
}
|
||||
|
||||
// Restore terminal state and close pool
|
||||
func (p *Pool) Stop() error {
|
||||
// Wait until one final refresh has passed.
|
||||
time.Sleep(p.RefreshRate)
|
||||
|
||||
p.finishOnce.Do(func() {
|
||||
close(p.quit)
|
||||
})
|
||||
return unlockEcho()
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
package pb
|
||||
|
||||
import (
|
||||
"io"
|
||||
)
|
||||
|
||||
// It's proxy reader, implement io.Reader
|
||||
type Reader struct {
|
||||
io.Reader
|
||||
bar *ProgressBar
|
||||
}
|
||||
|
||||
func (r *Reader) Read(p []byte) (n int, err error) {
|
||||
n, err = r.Reader.Read(p)
|
||||
r.bar.Add(n)
|
||||
return
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
package pb
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// Finds the control character sequences (like colors)
|
||||
var ctrlFinder = regexp.MustCompile("\x1b\x5b[0-9]+\x6d")
|
||||
|
||||
func escapeAwareRuneCountInString(s string) int {
|
||||
n := utf8.RuneCountInString(s)
|
||||
for _, sm := range ctrlFinder.FindAllString(s, -1) {
|
||||
n -= len(sm)
|
||||
}
|
||||
return n
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
package pb
|
||||
|
||||
import "testing"
|
||||
|
||||
func Test_RuneCount(t *testing.T) {
|
||||
s := string([]byte{
|
||||
27, 91, 51, 49, 109, // {Red}
|
||||
72, 101, 108, 108, 111, // Hello
|
||||
44, 32, // ,
|
||||
112, 108, 97, 121, 103, 114, 111, 117, 110, 100, // Playground
|
||||
27, 91, 48, 109, // {Reset}
|
||||
})
|
||||
if e, l := 17, escapeAwareRuneCountInString(s); l != e {
|
||||
t.Errorf("Invalid length %d, expected %d", l, e)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
// +build darwin freebsd netbsd openbsd solaris dragonfly
|
||||
|
||||
package pb
|
||||
|
||||
import "syscall"
|
||||
|
||||
const ioctlReadTermios = syscall.TIOCGETA
|
||||
const ioctlWriteTermios = syscall.TIOCSETA
|
|
@ -0,0 +1,6 @@
|
|||
// +build linux
|
||||
|
||||
package pb
|
||||
|
||||
const ioctlReadTermios = 0x5401 // syscall.TCGETS
|
||||
const ioctlWriteTermios = 0x5402 // syscall.TCSETS
|
Загрузка…
Ссылка в новой задаче