├── .gitignore ├── LICENSE ├── README.md ├── RELEASES.md ├── cursor_vt100.go ├── cursor_windows.go ├── example └── example.go ├── go.mod ├── go.sum ├── licenses ├── github.com │ ├── fatih │ │ └── color │ │ │ └── LICENSE.md │ ├── leaanthony │ │ ├── synx │ │ │ └── LICENSE │ │ └── wincursor │ │ │ └── LICENSE │ └── mattn │ │ ├── go-colorable │ │ └── LICENSE │ │ └── go-isatty │ │ └── LICENSE └── golang.org │ └── x │ └── sys │ └── LICENSE ├── spinner.go ├── spinner_mac.gif ├── spinner_ubuntu.gif └── spinner_windows.gif /.gitignore: -------------------------------------------------------------------------------- 1 | example/example 2 | /example/example.exe 3 | /.vscode 4 | /vendor -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Lea Anthony 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spinner 2 | 3 | A simple, configurable, multi-platform terminal spinner. 4 | 5 | [![MIT license](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/leaanthony/spinner/blob/master/LICENSE) 6 | [![GitHub version](https://badge.fury.io/gh/leaanthony%2Fspinner.svg)](https://github.com/leaanthony/spinner) 7 | [![Go Report Card](https://goreportcard.com/badge/github.com/leaanthony/spinner)](https://goreportcard.com/report/github.com/leaanthony/spinner) 8 | [![Godoc Reference](https://godoc.org/github.com/leaanthony/spinner?status.svg)](http://godoc.org/github.com/leaanthony/spinner) 9 | [![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](https://github.com/leaanthony/spinner/issues) 10 | ## Demo 11 | ![demo](spinner_mac.gif) 12 | 13 | Spinner running on a Mac. See the [Linux](#linux-demo) and [Windows](#windows-demo) demos. 14 | 15 | ## About 16 | 17 | Spinner is for giving the user visual feedback during processing in command line apps. It has the following features: 18 | 19 | * Works cross-platform 20 | * Completely customisable (messages, symbols, spinners) 21 | * Sensible defaults for non-VT100 systems 22 | * Smoooothe! (no ghost cursors, minimal text updates) 23 | * Minimal memory allocation (Reusable spinner) 24 | * Graceful handling of Ctrl-C interrupts 25 | 26 | Tested on: 27 | 28 | * Windows 10 29 | * MacOS 10.13.5 30 | * Ubuntu 18.04 LTS 31 | 32 | ## Installation 33 | 34 | ``` 35 | go get -u github.com/leaanthony/spinner 36 | ``` 37 | 38 | ## Usage 39 | 40 | ### New Spinner 41 | 42 | Spinners are created using New(optionalMessage string), which takes an optional message to display. 43 | 44 | ``` 45 | myspinner := spinner.New("Processing images") 46 | ``` 47 | or 48 | ``` 49 | myspinner := spinner.New() 50 | ``` 51 | 52 | ### Starting the spinner 53 | 54 | To start the spinner, simply call Start(optionalMessage string). If the optional message is passed, it will be used as the spinner message. 55 | 56 | ``` 57 | myspinner := spinner.New("Processing images") 58 | myspinner.Start() 59 | ``` 60 | is equivalent to 61 | ``` 62 | myspinner := spinner.New() 63 | myspinner.Start("Processing images") 64 | ``` 65 | It's possible to reuse an existing spinner by calling Start() on a spinner that has been previously stopped with Error() or Success(). 66 | 67 | ## Updating the spinner message 68 | 69 | The spinner message can be updated with UpdateMessage(string) whilst running. 70 | 71 | ``` 72 | myspinner := spinner.New() 73 | myspinner.Start("Processing images") 74 | time.Sleep(time.Second * 2) 75 | myspinner.UpdateMessage("Adding funny text") 76 | time.Sleep(time.Second * 2) 77 | myspinner.Success("Your memes are ready") 78 | ``` 79 | 80 | ### Stop with Success 81 | 82 | To stop the spinner and indicate successful completion, call the Success(optionalMessage string) method. This will print a success symbol with the original message - all in green. If the optional string is given, it will be used instead of the original message. 83 | 84 | ``` 85 | // Default 86 | myspinner := spinner.New("Processing images") 87 | myspinner.Start() 88 | time.Sleep(time.Second * 2) 89 | myspinner.Success() 90 | 91 | // Custom Message 92 | myspinner := spinner.New("Processing audio") 93 | myspinner.Start() 94 | time.Sleep(time.Second * 2) 95 | myspinner.Success("It took a while, but that was successful!") 96 | ``` 97 | 98 | ### Stop with Error 99 | 100 | To stop the spinner and indicate an error, call the Error(optionalMessage string) method. 101 | This has the same functionality as Success(), but will print an error symbol and it will all be in red. 102 | 103 | ``` 104 | // Default 105 | myspinner := spinner.New("Processing images") 106 | myspinner.Start() 107 | time.Sleep(time.Second * 2) 108 | myspinner.Error() 109 | 110 | // Custom message 111 | myspinner := spinner.New("Processing audio") 112 | myspinner.Start() 113 | time.Sleep(time.Second * 2) 114 | myspinner.Error("Too many lolcats!") 115 | ``` 116 | 117 | ### Success/Error using custom formatter 118 | 119 | In addition to Success() and Error(), there is Successf() and Errorf(). Both take the same parameters: (format string, args ...interface{}). This is identical to fmt.Sprintf (which it uses under the hood). 120 | 121 | ``` 122 | // Formatted Success 123 | a = spinner.New("This is a formatted custom success message") 124 | a.Start() 125 | time.Sleep(time.Second * 2) 126 | spin := "Spinner" 127 | awesome := "Awesome" 128 | a.Successf("%s is %s!", spin, awesome) 129 | 130 | // Formatted Error 131 | a = spinner.New("This is a formatted custom error message") 132 | a.Start() 133 | secs := 2 134 | time.Sleep(time.Second * time.Duration(secs)) 135 | a.Errorf("I waited %d seconds to error!", secs) 136 | ``` 137 | 138 | ### Custom Success/Error Symbols 139 | 140 | Both Success() and Error() use symbols (as well as colour) to indicate their status. 141 | These symbols default to spaces on Windows and ✓ & ✗ on other (vt100 compatible) platforms. They can be set manually using the SetSuccessSymbol(symbol string) and SetErrorSymbol(symbol string) methods. 142 | 143 | ``` 144 | myspinner := spinner.New("Processing images") 145 | 146 | // Custom symbols 147 | myspinner.SetErrorSymbol("💩") 148 | myspinner.SetSuccessSymbol("🏆") 149 | 150 | myspinner.Start() 151 | time.Sleep(time.Second * 2) 152 | myspinner.Error("It broke :(") 153 | ``` 154 | 155 | ### Custom Spinner 156 | 157 | By default, the spinner used is the spinning bar animation on Windows and the snake animation for other platforms. This can be customised using SetSpinFrames(frames []string). It takes a slice of strings defining the spinner symbols. 158 | 159 | ``` 160 | myspinner := spinner.New("Processing images") 161 | myspinner.SetSpinFrames([]string{"^", ">", "v", "<"}) 162 | myspinner.Start() 163 | time.Sleep(time.Second * 2) 164 | myspinner.Success() 165 | ``` 166 | 167 | ## Handling Ctrl-C Interrupts 168 | 169 | By default, Ctrl-C will error out the current spinner with the message "Aborted (ctrl-c)". A custom message can be set using SetAbortMessage(string). 170 | 171 | ``` 172 | myspinner := spinner.New("💣 Tick...tick...tick...") 173 | myspinner.SetAbortMessage("Defused!") 174 | myspinner.Start() 175 | time.Sleep(time.Second * 5) 176 | myspinner.Success("💥 Boom!") 177 | ``` 178 | 179 | ## Rationale 180 | 181 | I tried to find a simple, true cross platform spinner for Go that did what I wanted and couldn't find one. I'm sure they exist, but this was fun. 182 | 183 | ## Linux Demo 184 | ![demo](spinner_ubuntu.gif) 185 | 186 | ## Windows Demo 187 | ![demo](spinner_windows.gif) 188 | 189 | ## With a little help from my friends 190 | 191 | This project uses the awesome [Color] library. The code for handling windows cursor hiding and showing was split off into a different project ([wincursor]) 192 | 193 | [Color]: https://github.com/fatih/color 194 | [wincursor]: https://github.com/leaanthony/wincursor 195 | 196 | --- 197 | [![Maintenance](https://img.shields.io/badge/Maintained%3F-yes-green.svg)](https://github.com/leaanthony/spinner/graphs/commit-activity) 198 | [![HitCount](http://hits.dwyl.io/leaanthony/spinner.svg)](http://hits.dwyl.io/leaanthony/spinner) 199 | -------------------------------------------------------------------------------- /RELEASES.md: -------------------------------------------------------------------------------- 1 | # Releases 2 | 3 | ## 0.5.0 - WIP 4 | * Trim messages to fit console width 5 | * Attempts to handle console resizes 6 | * Fixed race conditions: Dynamic updates safe 7 | 8 | ## 0.4 - 17 July 2018 9 | Added UpdateMessage(string) 10 | Handle ctrl-c interrupts 11 | Added SetAbortMessage(string) to set message shown on ctrl-c 12 | Updated documentation 13 | 14 | ## 0.3 - 15 July 2018 15 | Made message for New/NewSpinner optional 16 | Start() now takes an optional message 17 | Removed Restart() 18 | Use '>' as defualt success symbol on Windows 19 | Use '!' as defualt error symbol on Windows 20 | 21 | ## 0.2.1 - 11 July 2018 22 | Issue warning if attempting to stop a non-running spinner 23 | 24 | ## 0.2 - 10 July 2018 25 | Added custom Sussess/Error functions: Successf() and Error() 26 | 27 | ## 0.1 - 6 July 2018 28 | Initial Release 29 | -------------------------------------------------------------------------------- /cursor_vt100.go: -------------------------------------------------------------------------------- 1 | // +build !windows 2 | 3 | package spinner 4 | 5 | import ( 6 | "fmt" 7 | ) 8 | 9 | func init() { 10 | 11 | } 12 | 13 | func showCursor() { 14 | fmt.Printf("\033[?25h") 15 | } 16 | 17 | func hideCursor() { 18 | fmt.Printf("\033[?25l") 19 | } 20 | 21 | func (s *Spinner) clearCurrentLine() { 22 | fmt.Printf("\r\033[0K") 23 | } 24 | -------------------------------------------------------------------------------- /cursor_windows.go: -------------------------------------------------------------------------------- 1 | // +build windows 2 | 3 | package spinner 4 | 5 | import ( 6 | "fmt" 7 | 8 | "github.com/leaanthony/wincursor" 9 | ) 10 | 11 | func showCursor() { 12 | wincursor.Show() 13 | } 14 | 15 | func hideCursor() { 16 | wincursor.Hide() 17 | } 18 | 19 | func (s *Spinner) clearCurrentLine() { 20 | // *shudder* 21 | fmt.Printf("\r") 22 | 23 | // Get the current line length 24 | var length = len(s.getMessage()) + len(s.getCurrentSpinnerFrame()) + 1 25 | 26 | for i := 0; i < length; i++ { 27 | fmt.Printf(" ") 28 | } 29 | fmt.Printf("\r") 30 | } 31 | -------------------------------------------------------------------------------- /example/example.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "runtime" 6 | "time" 7 | 8 | "github.com/leaanthony/spinner" 9 | ) 10 | 11 | func main() { 12 | 13 | // Default Success 14 | a := spinner.New("This is a success") 15 | a.Start() 16 | time.Sleep(time.Second * 2) 17 | a.Success() 18 | 19 | // Default Error 20 | a = spinner.New("This is an error") 21 | a.Start() 22 | time.Sleep(time.Second * 2) 23 | a.Error() 24 | 25 | // Custom Success 26 | a = spinner.New("This is a custom success message") 27 | a.Start() 28 | time.Sleep(time.Second * 2) 29 | a.Success("Awesome!") 30 | 31 | // Custom Error 32 | a = spinner.New("This is a custom error message") 33 | a.Start() 34 | time.Sleep(time.Second * 2) 35 | a.Error("Much sad") 36 | 37 | // Formatted Success 38 | a = spinner.New("This is a formatted custom success message") 39 | a.Start() 40 | time.Sleep(time.Second * 2) 41 | spin := "Spinner" 42 | awesome := "Awesome" 43 | a.Successf("%s is %s!", spin, awesome) 44 | 45 | // Formatted Error 46 | a = spinner.New("This is a formatted custom error message") 47 | a.Start() 48 | secs := 2 49 | time.Sleep(time.Second * time.Duration(secs)) 50 | a.Errorf("I waited %d seconds to error!", secs) 51 | 52 | // Reuse spinner! 53 | a.Start("Spinner reuse FTW!") 54 | time.Sleep(time.Second * 2) 55 | a.Success() 56 | 57 | // Spinner frame madness 58 | a = spinner.New("Change spinners on the fly") 59 | a.Start() 60 | time.Sleep(time.Second * 2) 61 | a.SetSpinFrames([]string{"+", "x", "X", "x"}) 62 | time.Sleep(time.Second * 2) 63 | a.SetSpinFrames([]string{"\\", "|", "/", "-"}) 64 | time.Sleep(time.Second * 2) 65 | a.SetSpinFrames([]string{"--> ", " --> ", " -->"}) 66 | time.Sleep(time.Second * 2) 67 | a.Success() 68 | 69 | // Spinner timer awesomeness 70 | msg := "Change spinner timing on the fly: Normal" 71 | a = spinner.New(msg) 72 | a.Start() 73 | time.Sleep(time.Second * 2) 74 | msg += " Slow" 75 | a.SetSpinSpeed(300) 76 | a.UpdateMessage(msg) 77 | time.Sleep(time.Second * 2) 78 | msg += " Normal" 79 | a.SetSpinSpeed(100) 80 | a.UpdateMessage(msg) 81 | time.Sleep(time.Second * 2) 82 | msg += " Fast" 83 | a.SetSpinSpeed(50) 84 | a.UpdateMessage(msg) 85 | time.Sleep(time.Second * 2) 86 | a.Success(msg + ". Much Wow.") 87 | 88 | // Spinner with no initial message 89 | a = spinner.New() 90 | a.Start("Message is now optional on Spinner creation") 91 | time.Sleep(time.Second * 2) 92 | a.Success("Awesome! More flexibility!") 93 | 94 | // Custom Spinner chars + symbols 95 | switch runtime.GOOS { 96 | case "windows": 97 | a.SetSpinFrames([]string{"^", ">", "v", "<"}) 98 | a.SetSuccessSymbol("+") 99 | default: 100 | a.SetSpinFrames([]string{"🌕", "🌖", "🌗", "🌘", "🌑", "🌒", "🌓", "🌔"}) 101 | a.SetSuccessSymbol("👍") 102 | } 103 | a.Start("Custom spinner + Success Symbol!") 104 | time.Sleep(time.Second * 2) 105 | a.Success() 106 | 107 | // Custom Spinner chars + symbols 108 | switch runtime.GOOS { 109 | case "windows": 110 | a.SetSpinFrames([]string{".", "o", "O", "@", "*"}) 111 | a.SetErrorSymbol("!") 112 | default: 113 | a.SetSpinFrames([]string{"🕐", "🕑", "🕒", "🕓", "🕔", "🕕", "🕖", "🕗", "🕘", "🕙", "🕚", "🕛"}) 114 | a.SetErrorSymbol("💩") 115 | } 116 | a.Start("Custom spinner + Error Symbol!") 117 | time.Sleep(time.Second * 2) 118 | a.Error() 119 | 120 | // Updating messages 121 | updateMessage := "2" 122 | a.Start(updateMessage) 123 | time.Sleep(time.Second * 1) 124 | updateMessage += " 4" 125 | a.UpdateMessage(updateMessage) 126 | time.Sleep(time.Second * 1) 127 | updateMessage += " 6" 128 | a.UpdateMessage(updateMessage) 129 | time.Sleep(time.Second * 1) 130 | updateMessage += " 8" 131 | a.UpdateMessage(updateMessage) 132 | time.Sleep(time.Second * 1) 133 | updateMessage += " Motorway!" 134 | a.Success(updateMessage) 135 | 136 | fmt.Println("") 137 | fmt.Println("If we stop a non-running spinner it should issue a warning.") 138 | fmt.Println("Next we will check that all stop-related functions issue the warning.") 139 | fmt.Println("") 140 | 141 | // Ensure we don't hang if calling success/error on non-running spinner 142 | a = spinner.New("Test Success()") 143 | a.Success() 144 | a = spinner.New("Test Error()") 145 | a.Error() 146 | a = spinner.New("Test Custom messages") 147 | a.Success(`Test Success("")`) 148 | a.Error(`Test Error("")`) 149 | a.Successf(`Test Successf("")`) 150 | a.Errorf(`Test Errorf("")`) 151 | 152 | // Interrupt handling 153 | fmt.Println("") 154 | fmt.Println("Interrupt handling. Hit Ctrl-C to stop bomb exploding!") 155 | fmt.Println("") 156 | 157 | a = spinner.New("💣 Tick...tick...tick...") 158 | a.SetAbortMessage("Defused!") 159 | a.Start() 160 | time.Sleep(time.Second * 5) 161 | a.Success("💥 Boom!") 162 | } 163 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/leaanthony/spinner 2 | 3 | require ( 4 | github.com/fatih/color v1.7.0 5 | github.com/leaanthony/synx v0.1.0 6 | github.com/leaanthony/wincursor v0.1.0 7 | github.com/mattn/go-isatty v0.0.4 8 | ) 9 | 10 | require ( 11 | github.com/mattn/go-colorable v0.0.9 // indirect 12 | golang.org/x/sys v0.1.0 // indirect 13 | ) 14 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= 2 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 3 | github.com/leaanthony/synx v0.1.0 h1:R0lmg2w6VMb8XcotOwAe5DLyzwjLrskNkwU7LLWsyL8= 4 | github.com/leaanthony/synx v0.1.0/go.mod h1:Iz7eybeeG8bdq640iR+CwYb8p+9EOsgMWghkSRyZcqs= 5 | github.com/leaanthony/wincursor v0.1.0 h1:Dsyp68QcF5cCs65AMBmxoYNEm0n8K7mMchG6a8fYxf8= 6 | github.com/leaanthony/wincursor v0.1.0/go.mod h1:7TVwwrzSH/2Y9gLOGH+VhA+bZhoWXBRgbGNTMk+yimE= 7 | github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= 8 | github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= 9 | github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs= 10 | github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 11 | golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= 12 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 13 | -------------------------------------------------------------------------------- /licenses/github.com/fatih/color/LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Fatih Arslan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /licenses/github.com/leaanthony/synx/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018-Present Lea Anthony 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 13 | all 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 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /licenses/github.com/leaanthony/wincursor/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018-Present Lea Anthony 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 13 | all 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 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /licenses/github.com/mattn/go-colorable/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Yasuhiro Matsumoto 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 | -------------------------------------------------------------------------------- /licenses/github.com/mattn/go-isatty/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) Yasuhiro MATSUMOTO 2 | 3 | MIT License (Expat) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /licenses/golang.org/x/sys/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009 The Go Authors. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name of Google Inc. nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /spinner.go: -------------------------------------------------------------------------------- 1 | // spinner provides visual feedback for command line applications 2 | 3 | package spinner 4 | 5 | import ( 6 | "fmt" 7 | "os" 8 | "os/signal" 9 | "runtime" 10 | "strings" 11 | "time" 12 | 13 | "github.com/fatih/color" 14 | "github.com/leaanthony/synx" 15 | isatty "github.com/mattn/go-isatty" 16 | ) 17 | 18 | // Specialise the type 19 | type status int 20 | 21 | // Status code constants. 22 | const ( 23 | errorStatus status = iota 24 | successStatus 25 | ) 26 | 27 | // Gets the default spinner frames based on the operating system. 28 | func getDefaultSpinnerFrames() []string { 29 | switch runtime.GOOS { 30 | case "windows": 31 | return []string{"|", "/", "-", "\\"} 32 | default: 33 | return []string{"⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷"} 34 | } 35 | } 36 | 37 | // Gets the default status symbols based on the operating system. 38 | func getStatusSymbols() (successSymbol, errorSymbol string) { 39 | switch runtime.GOOS { 40 | case "windows": 41 | return ">", "!" 42 | default: 43 | return "✓", "✗" 44 | } 45 | } 46 | 47 | // Spinner defines our spinner data. 48 | type Spinner struct { 49 | message *synx.String // message to display 50 | stopChan chan struct{} // exit channel 51 | speedUpdated *synx.Bool // Indicates speed has been updated 52 | exitStatus status // Status of exit 53 | successSymbol *synx.String // Symbol printed when Success() called 54 | errorSymbol *synx.String // Symbol printed when Error() called 55 | spinFrames *synx.StringSlice // Spinset frames 56 | frameNumber int // Current frame [default 0] 57 | termWidth *synx.Int // Terminal width 58 | termHeight *synx.Int // Terminal Height 59 | spinSpeed *synx.Int // Delay between spinner updates in milliseconds [default 100ms] 60 | currentLine *synx.String // The current line being displayed 61 | running *synx.Bool // Indicates if the spinner is running 62 | abortMessage *synx.String // Printed when handling ctrl-c interrupt 63 | isTerminal *synx.Bool // Flag indicating if we are outputting to terminal 64 | exitOnInterrupt *synx.Bool // Indicates the spinner will os.Exit on interrupt 65 | } 66 | 67 | // NewSpinner creates a new spinner and sets up the default values. 68 | func NewSpinner(optionalMessage ...string) *Spinner { 69 | successSymbol, errorSymbol := getStatusSymbols() 70 | // Blank message by default 71 | message := "" 72 | if len(optionalMessage) > 0 { 73 | message = optionalMessage[0] 74 | } 75 | result := &Spinner{ 76 | message: synx.NewString(message), 77 | stopChan: make(chan struct{}), 78 | speedUpdated: synx.NewBool(true), 79 | successSymbol: synx.NewString(successSymbol), 80 | errorSymbol: synx.NewString(errorSymbol), 81 | spinFrames: synx.NewStringSlice(getDefaultSpinnerFrames()), 82 | spinSpeed: synx.NewInt(100), 83 | termWidth: synx.NewInt(1), 84 | termHeight: synx.NewInt(1), 85 | abortMessage: synx.NewString("Aborted."), 86 | frameNumber: 0, 87 | running: synx.NewBool(false), 88 | isTerminal: synx.NewBool(isatty.IsTerminal(os.Stdout.Fd())), 89 | exitOnInterrupt: synx.NewBool(true), 90 | } 91 | 92 | return result 93 | } 94 | 95 | // New is solely here to make code cleaner for importers. 96 | // EG: spinner.New(...) 97 | func New(message ...string) *Spinner { 98 | return NewSpinner(message...) 99 | } 100 | 101 | // SetSuccessSymbol sets the symbol displayed on success. 102 | func (s *Spinner) SetSuccessSymbol(symbol string) { 103 | s.successSymbol.SetValue(symbol) 104 | } 105 | 106 | // getSuccessSymbol sets the symbol displayed on error. 107 | func (s *Spinner) getSuccessSymbol() string { 108 | return s.successSymbol.GetValue() 109 | } 110 | 111 | // SetErrorSymbol sets the symbol displayed on error. 112 | func (s *Spinner) SetErrorSymbol(symbol string) { 113 | s.errorSymbol.SetValue(symbol) 114 | } 115 | 116 | // getErrorSymbol sets the symbol displayed on error. 117 | func (s *Spinner) getErrorSymbol() (symbol string) { 118 | return s.errorSymbol.GetValue() 119 | } 120 | 121 | // SetSpinFrames makes the spinner use the given characters. 122 | func (s *Spinner) SetSpinFrames(frames []string) { 123 | s.spinFrames.SetValue(frames) 124 | } 125 | 126 | func (s *Spinner) getNextSpinnerFrame() (result string) { 127 | // Check if the current frame is valid. If not, loop to start 128 | s.frameNumber = s.frameNumber % s.spinFrames.Length() 129 | result = s.spinFrames.GetElement(s.frameNumber) 130 | s.frameNumber++ 131 | return 132 | } 133 | 134 | func (s *Spinner) getCurrentSpinnerFrame() (result string) { 135 | s.frameNumber = s.frameNumber % s.spinFrames.Length() 136 | result = s.spinFrames.GetElement(s.frameNumber) 137 | return result 138 | } 139 | 140 | // SetSpinSpeed sets the speed of the spinner animation. 141 | // The lower the value, the faster the spin. 142 | func (s *Spinner) SetSpinSpeed(ms int) { 143 | // Floor to a speed of 1 144 | if ms < 1 { 145 | ms = 1 146 | } 147 | s.spinSpeed.SetValue(ms) 148 | s.speedUpdated.SetValue(true) 149 | } 150 | 151 | // getSpinSpeed gets the speed of the spinner animation. 152 | func (s *Spinner) getSpinSpeed() (ms int) { 153 | return s.spinSpeed.GetValue() 154 | } 155 | 156 | // UpdateMessage sets the spinner message. 157 | // Can be flickery if not appending so use with care. 158 | func (s *Spinner) UpdateMessage(message string) { 159 | // Clear line if this isn't an append. 160 | // for smoother screen updates. 161 | if strings.Index(message, s.getMessage()) != 0 { 162 | s.clearCurrentLine() 163 | } 164 | s.setMessage(message) 165 | } 166 | 167 | // SetAbortMessage sets the message that gets printed when 168 | // the user kills the spinners by pressing ctrl-c. 169 | func (s *Spinner) SetAbortMessage(message string) { 170 | s.abortMessage.SetValue(message) 171 | } 172 | 173 | func (s *Spinner) getAbortMessage() string { 174 | return s.abortMessage.GetValue() 175 | } 176 | 177 | func (s *Spinner) setMessage(message string) { 178 | s.message.SetValue(message) 179 | } 180 | 181 | func (s *Spinner) getMessage() string { 182 | return s.message.GetValue() 183 | } 184 | 185 | func (s *Spinner) getRunning() bool { 186 | return s.running.GetValue() 187 | } 188 | 189 | func (s *Spinner) setRunning(value bool) { 190 | s.running.SetValue(value) 191 | } 192 | func (s *Spinner) getExitOnInterrupt() bool { 193 | return s.exitOnInterrupt.GetValue() 194 | } 195 | 196 | func (s *Spinner) SetExitOnInterrupt(value bool) { 197 | s.exitOnInterrupt.SetValue(value) 198 | } 199 | 200 | func (s *Spinner) printSuccess(message string, args ...interface{}) { 201 | color.HiGreen(message, args...) 202 | } 203 | 204 | // Start the spinner! 205 | func (s *Spinner) Start(optionalMessage ...string) { 206 | // If we're trying to start an already running spinner, 207 | // add a slight delay and retry. This allows the spinner 208 | // to complete a previous stop command gracefully. 209 | count := 0 210 | maxCount := 10 211 | for s.getRunning() == true && count < maxCount { 212 | // 213 | time.Sleep(time.Millisecond * 50) 214 | count++ 215 | } 216 | 217 | // Did we fail? 218 | if count == maxCount { 219 | s.Error("Tried to start a running spinner with message: " + s.getMessage()) 220 | return 221 | } 222 | 223 | // If we have a message, set it 224 | if len(optionalMessage) > 0 { 225 | s.setMessage(optionalMessage[0]) 226 | } 227 | 228 | // make it look tidier. 229 | hideCursor() 230 | 231 | // Store the fact we are now running. 232 | s.setRunning(true) 233 | 234 | // Handle ctrl-c 235 | go func(stopChan chan struct{}) { 236 | sigchan := make(chan os.Signal, 10) 237 | signal.Notify(sigchan, os.Interrupt) 238 | <-sigchan 239 | // Notify and clean up 240 | s.stopChan <- struct{}{} 241 | fmt.Println("") 242 | color.HiRed("\r%s %s", s.getErrorSymbol(), s.getAbortMessage()) 243 | if s.getExitOnInterrupt() { 244 | os.Exit(1) 245 | } 246 | }(s.stopChan) 247 | 248 | // spawn off a goroutine to handle the animation. 249 | go func() { 250 | 251 | ticker := time.NewTicker(time.Millisecond * time.Duration(s.spinSpeed.GetValue())) 252 | 253 | // Let's go! 254 | for { 255 | select { 256 | // For each frame tick 257 | case <-ticker.C: 258 | // Rewind to start of line and print the current frame and message. 259 | // Note: We don't fully clear the line here as this causes flickering. 260 | fmt.Printf("\r") 261 | fmt.Printf("%s %s", s.getNextSpinnerFrame(), s.getMessage()) 262 | 263 | // Do we need to update the ticker? 264 | if s.speedUpdated.GetValue() == true { 265 | ticker.Stop() 266 | ticker = time.NewTicker(time.Millisecond * time.Duration(s.spinSpeed.GetValue())) 267 | } 268 | 269 | // If we get a stop signal 270 | case <-s.stopChan: 271 | 272 | // Store the fact we aren't running 273 | s.setRunning(false) 274 | 275 | // Quit the animation 276 | return 277 | } 278 | } 279 | }() 280 | } 281 | 282 | // stop will stop the spinner. 283 | // The final message will either be the current message 284 | // or the optional, given message. 285 | // Success status will print the message in green. 286 | // Error status will print the message in red. 287 | func (s *Spinner) stop(message ...string) { 288 | 289 | var finalMessage = s.getMessage() 290 | 291 | // If we have an optional message, save it. 292 | if len(message) > 0 { 293 | finalMessage = message[0] 294 | } 295 | 296 | // Ensure we are running before issuing stop signal. 297 | if s.running.GetValue() { 298 | // Issue stop signal to animation. 299 | s.stopChan <- struct{}{} 300 | } 301 | 302 | // Clear the line, because a new message may be shorter than the original. 303 | s.clearCurrentLine() 304 | 305 | // Output the symbol and message depending on the status code. 306 | if s.exitStatus == errorStatus { 307 | color.HiRed("\r%s %s", s.getErrorSymbol(), finalMessage) 308 | } else { 309 | color.HiGreen("\r%s %s", s.getSuccessSymbol(), finalMessage) 310 | } 311 | 312 | // Show the cursor again 313 | showCursor() 314 | } 315 | 316 | // Error stops the spinner and sets the status code to error. 317 | // Optional message to print instead of current message. 318 | func (s *Spinner) Error(message ...string) { 319 | s.exitStatus = errorStatus 320 | s.stop(message...) 321 | } 322 | 323 | // Errorf stops the spinner, formats and sets the status code to error. 324 | // Formats and prints the given message instead of current message. 325 | func (s *Spinner) Errorf(format string, args ...interface{}) { 326 | s.exitStatus = errorStatus 327 | message := fmt.Sprintf(format, args...) 328 | s.stop(message) 329 | } 330 | 331 | // Success stops the spinner and sets the status code to success. 332 | // Optional message to print instead of current message. 333 | func (s *Spinner) Success(message ...string) { 334 | s.exitStatus = successStatus 335 | s.stop(message...) 336 | } 337 | 338 | // Successf stops the spinner, formats and sets the status code to success. 339 | // Formats and prints the given message instead of current message. 340 | func (s *Spinner) Successf(format string, args ...interface{}) { 341 | s.exitStatus = successStatus 342 | message := fmt.Sprintf(format, args...) 343 | s.stop(message) 344 | } 345 | -------------------------------------------------------------------------------- /spinner_mac.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leaanthony/spinner/beb9632a9c158a2259a60f3aa6e44586d7529ff0/spinner_mac.gif -------------------------------------------------------------------------------- /spinner_ubuntu.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leaanthony/spinner/beb9632a9c158a2259a60f3aa6e44586d7529ff0/spinner_ubuntu.gif -------------------------------------------------------------------------------- /spinner_windows.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leaanthony/spinner/beb9632a9c158a2259a60f3aa6e44586d7529ff0/spinner_windows.gif --------------------------------------------------------------------------------