├── .gitignore ├── .gitmodules ├── LICENSE.md ├── README.rst ├── main.go ├── run_tests ├── tests ├── print-and-sleep-on-input-o-will-produce-spinning.test.sh ├── print-and-sleep-on-input-with-flag-o-will-produce-spinning-and-stdout.test.sh ├── print-and-sleep-on-input-with-flag-status-will-produce-spinnint-with-status.test.sh ├── setup.sh └── teardown.sh └── vendor └── .gitignore /.gitignore: -------------------------------------------------------------------------------- 1 | /spin 2 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "vendor/github.com/reconquest/import.bash"] 2 | path = vendor/github.com/reconquest/import.bash 3 | url = https://github.com/reconquest/import.bash 4 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Egor Kovetskiy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | **** 2 | spin 3 | **** 4 | 5 | .. image:: http://i.imgur.com/JyfyJg9.gif 6 | :alt: usage example 7 | 8 | The universal tool that provides the dead simple progress indicator. 9 | 10 | Installation 11 | ============ 12 | 13 | **spin** is go-gettable:: 14 | 15 | go get github.com/kovetskiy/spin 16 | 17 | Usage 18 | ===== 19 | 20 | See ``spin --help`` for command line options. 21 | 22 | ``-i --stdin-as-indicator`` Use stdin data as progress indicator. 23 | 24 | ``-o --write-stdin`` Write stdin to spinner's stdout on exit. 25 | 26 | ``-s --status `` Use specified ```` as spinner status. 27 | 28 | ``-t --interval `` Use specified ```` as spinner iteration interval. [default: 100] 29 | 30 | All you need to do is pipe any command to spin as follows:: 31 | 32 | sleep 2 | spin -s 'Loading... ' 33 | 34 | and your shell will spawn ``spin`` process and terminate it when ``sleep`` exited. 35 | 36 | If you want to indicate real progress you can use flag ``-i`` and spin will 37 | use stdin data as progress indicator:: 38 | 39 | git clone --progress https://github.com/kovetskiy/dotfiles 2>&1 | spin -i -s 'Cloning... ' 40 | 41 | License 42 | ======= 43 | 44 | MIT. 45 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "log" 8 | "os" 9 | "strconv" 10 | "strings" 11 | "time" 12 | 13 | "github.com/kovetskiy/godocs" 14 | "github.com/kovetskiy/spinner-go" 15 | ) 16 | 17 | var ( 18 | version = `1` 19 | usage = `spin ` + version + ` 20 | 21 | Usage: 22 | spin [options] 23 | 24 | Options: 25 | -i --stdin-as-indicator Use stdin data as progress indicator. 26 | -o --write-stdin Write stdin to spinner's stdout on exit. 27 | -s --status Use specified as spinner status. 28 | -t --interval Use specified as spinner iteration interval. 29 | [default: 100] 30 | -l --line Use last line as status. 31 | -h --help Show this screen. 32 | --version Show version. 33 | ` 34 | ) 35 | 36 | func main() { 37 | args := godocs.MustParse(usage, version, godocs.UsePager) 38 | 39 | var ( 40 | watchStdin = args["--stdin-as-indicator"].(bool) 41 | writeStdin = args["--write-stdin"].(bool) 42 | writeLine = args["--line"].(bool) 43 | status, _ = args["--status"].(string) 44 | interval, _ = strconv.Atoi(args["--interval"].(string)) 45 | 46 | ticker = time.NewTicker( 47 | time.Duration(int64(interval)) * time.Millisecond, 48 | ) 49 | stdout = bytes.NewBuffer(nil) 50 | ) 51 | 52 | spinner.SetStatus(status).SetOutput(os.Stdout).SetActive(true) 53 | 54 | if status != "" { 55 | fmt.Print(status) 56 | } 57 | 58 | defer func() { 59 | spinner.SetActive(false) 60 | spinner.Spin() 61 | 62 | if writeStdin { 63 | fmt.Print(stdout.String()) 64 | } 65 | }() 66 | 67 | stdin, errors := getStdin() 68 | 69 | var ticking bool 70 | var reading bool 71 | 72 | for { 73 | select { 74 | case <-ticker.C: 75 | ticking = true 76 | 77 | case buffer := <-stdin: 78 | if writeStdin { 79 | stdout.WriteString(buffer) 80 | } 81 | 82 | if writeLine { 83 | lines := strings.Split(strings.TrimSpace(buffer), "\n") 84 | status := strings.TrimSpace(lines[len(lines)-1]) 85 | spinner.SetStatus(" " + status) 86 | } 87 | 88 | reading = true 89 | 90 | case err := <-errors: 91 | if err == io.EOF { 92 | return 93 | } 94 | 95 | log.Println(err) 96 | } 97 | 98 | if ticking && ((watchStdin && reading) || (!watchStdin)) { 99 | spinner.Spin() 100 | 101 | ticking = false 102 | reading = false 103 | } 104 | } 105 | 106 | } 107 | 108 | func getStdin() (chan string, chan error) { 109 | var ( 110 | reader = make(chan string) 111 | errorer = make(chan error) 112 | ) 113 | 114 | go func() { 115 | for { 116 | buffer := make([]byte, 0xFFFF) 117 | size, err := os.Stdin.Read(buffer) 118 | if err != nil { 119 | errorer <- err 120 | return 121 | } 122 | 123 | reader <- string(buffer[0:size]) 124 | } 125 | }() 126 | 127 | return reader, errorer 128 | } 129 | -------------------------------------------------------------------------------- /run_tests: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euo pipefail 4 | 5 | cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" 6 | source "vendor/github.com/reconquest/import.bash/import.bash" 7 | 8 | import:source github.com/reconquest/test-runner.bash 9 | 10 | go build -o spin.test 11 | 12 | :cleanup() { 13 | rm -r spin.test 14 | } 15 | 16 | trap :cleanup EXIT 17 | 18 | test-runner:set-testcases-dir tests 19 | test-runner:run "${@}" 20 | -------------------------------------------------------------------------------- /tests/print-and-sleep-on-input-o-will-produce-spinning.test.sh: -------------------------------------------------------------------------------- 1 | :spin <<< "echo 1; sleep 2; echo 2" 2 | 3 | :assert-output <